diff --git a/Project.toml b/Project.toml index 3f09555..ae6454e 100644 --- a/Project.toml +++ b/Project.toml @@ -1,17 +1,15 @@ name = "AcousticMetrics" uuid = "046f749b-9c1e-43ca-86bc-6902340f753e" -authors = ["Ingraham, Daniel James (GRC-LTV0) "] -version = "0.6.1" +authors = ["Ingraham, Daniel James (GRC-LTV0) and contributors"] +version = "0.7.0" [deps] -ConcreteStructs = "2569d6c7-a4a2-43d3-a901-331e8e4be471" FFTW = "7a1cc6ca-52ef-59f5-83cd-3a7055c09341" +FLOWMath = "6cb5d3fb-0fe8-4cc2-bd89-9fe0b19a99d3" ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" -OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" [compat] -julia = "1.8.2" -ConcreteStructs = "0.2.2" FFTW = "1.4.3" +FLOWMath = "0.3.3" ForwardDiff = "0.10.19" -OffsetArrays = "1.10.4" +julia = "1.8.2" diff --git a/README.md b/README.md index 9917257..ea085a5 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Currently implemented metrics include: * Approximate octave and third-octave spectra * Exact proportional spectra of any octave fraction > 0. + * Lazy representations of proportional band spectra constructed from either other narrowband or proportional band spectra * Integrated metrics diff --git a/docs/make.jl b/docs/make.jl index 57716a2..dbfcd52 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,7 +1,8 @@ module AMDocs using Documenter, AcousticMetrics +using AcousticMetrics: AcousticMetrics -function main() +function doit() IN_CI = get(ENV, "CI", nothing)=="true" makedocs(sitename="AcousticMetrics.jl", modules=[AcousticMetrics], doctest=false, @@ -17,7 +18,7 @@ function main() end if !isinteractive() - main() + doit() end end # module diff --git a/docs/src/api.md b/docs/src/api.md index 2925ae4..b9a6038 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -1,9 +1,92 @@ ```@meta CurrentModule = AMDocs ``` - # API Reference -```@autodocs -Modules = [AcousticMetrics] +## Fourier Transforms +```@docs +AcousticMetrics.rfft! +AcousticMetrics.irfft! +``` + +## Pressure Time History +```@docs +AbstractPressureTimeHistory +PressureTimeHistory +AcousticMetrics.pressure +AcousticMetrics.inputlength +AcousticMetrics.timestep(pth::AbstractPressureTimeHistory) +AcousticMetrics.starttime(pth::AbstractPressureTimeHistory) +AcousticMetrics.time +``` + +## Narrowband Metrics +```@docs +AbstractNarrowbandSpectrum +AcousticMetrics.halfcomplex +AcousticMetrics.timestep(sm::AbstractNarrowbandSpectrum) +AcousticMetrics.starttime(sm::AbstractNarrowbandSpectrum) +AcousticMetrics.samplerate +AcousticMetrics.frequency +AcousticMetrics.frequencystep +AcousticMetrics.istonal +PressureSpectrumAmplitude +PressureSpectrumPhase +MSPSpectrumAmplitude +MSPSpectrumPhase +PowerSpectralDensityAmplitude +PowerSpectralDensityPhase +``` + +### Proportional Bands and Proportional Band Spectra +```@docs +AbstractProportionalBands +AcousticMetrics.octave_fraction +AcousticMetrics.lower_center_upper +AcousticMetrics.freq_scaler +AcousticMetrics.band_start +AcousticMetrics.band_end +AcousticMetrics.lower_bands +AcousticMetrics.upper_bands +AcousticMetrics.center_bands +AcousticMetrics.cband_number +ExactProportionalBands +ExactOctaveCenterBands +ExactOctaveLowerBands +ExactOctaveUpperBands +ExactThirdOctaveCenterBands +ExactThirdOctaveLowerBands +ExactThirdOctaveUpperBands +ApproximateThirdOctaveBands +ApproximateThirdOctaveCenterBands +ApproximateThirdOctaveLowerBands +ApproximateThirdOctaveUpperBands +ApproximateOctaveBands +ApproximateOctaveCenterBands +ApproximateOctaveLowerBands +ApproximateOctaveUpperBands +AbstractProportionalBandSpectrum +AcousticMetrics.has_observer_time +AcousticMetrics.observer_time +AcousticMetrics.timestep(pbs::AbstractProportionalBandSpectrum) +AcousticMetrics.amplitude +AcousticMetrics.time_period +AcousticMetrics.time_scaler +LazyNBProportionalBandSpectrum +AcousticMetrics.frequency_nb +AcousticMetrics.lazy_pbs +ProportionalBandSpectrum +LazyPBSProportionalBandSpectrum +ProportionalBandSpectrumWithTime +AcousticMetrics.combine +``` + +## Weighting +```@docs +W_A +``` + +## Integrated Metrics +```@docs +OASPL ``` diff --git a/docs/src/index.md b/docs/src/index.md index 20a3074..f6c96df 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -15,7 +15,8 @@ Currently implemented metrics include: * Proportional band spectra * Approximate octave and third-octave spectra - * Exact proportional spectra of any octave fraction > 0. + * Exact proportional spectra of any octave fraction integer > 0. + * Lazy representations of proportional band spectra constructed from either narrowband or proportional band spectra * Integrated metrics diff --git a/src/AcousticMetrics.jl b/src/AcousticMetrics.jl index 69c50ec..fee7a03 100644 --- a/src/AcousticMetrics.jl +++ b/src/AcousticMetrics.jl @@ -1,17 +1,33 @@ module AcousticMetrics -using ConcreteStructs: @concrete +using Base.Iterators: Iterators +using Base.Order: ord, Forward using FFTW: r2r!, R2HC, HC2R, rfftfreq +using FLOWMath: abs_cs_safe using ForwardDiff: ForwardDiff -using OffsetArrays: OffsetArray include("constants.jl") + include("fourier_transforms.jl") + include("narrowband.jl") export AbstractPressureTimeHistory, PressureTimeHistory +export AbstractNarrowbandSpectrum +export PressureSpectrumAmplitude, PressureSpectrumPhase, MSPSpectrumAmplitude, MSPSpectrumPhase, PowerSpectralDensityAmplitude, PowerSpectralDensityPhase + +include("integrated.jl") +export OASPL include("proportional_bands.jl") +export AbstractProportionalBands +export ExactProportionalBands +export ExactOctaveCenterBands, ExactOctaveLowerBands, ExactOctaveUpperBands +export ExactThirdOctaveCenterBands, ExactThirdOctaveLowerBands, ExactThirdOctaveUpperBands +export ApproximateOctaveBands, ApproximateOctaveCenterBands, ApproximateOctaveLowerBands, ApproximateOctaveUpperBands +export ApproximateThirdOctaveBands, ApproximateThirdOctaveCenterBands, ApproximateThirdOctaveLowerBands, ApproximateThirdOctaveUpperBands +export AbstractProportionalBandSpectrum, LazyNBProportionalBandSpectrum, ProportionalBandSpectrum, ProportionalBandSpectrumWithTime, LazyPBSProportionalBandSpectrum include("weighting.jl") +export W_A end # module diff --git a/src/fourier_transforms.jl b/src/fourier_transforms.jl index 55e8c47..23a2dd5 100644 --- a/src/fourier_transforms.jl +++ b/src/fourier_transforms.jl @@ -1,123 +1,6 @@ -""" - dft_r2hc(x::AbstractVector) - -Calculate the real-input discrete Fourier transform, returning the result in the "half-complex" format. - -See -http://www.fftw.org/fftw3_doc/The-1d-Real_002ddata-DFT.html#The-1d-Real_002ddata-DFT -and http://www.fftw.org/fftw3_doc/The-Halfcomplex_002dformat-DFT.html for -details. - -Only use this for checking the derivatives of the FFT routines (should work fine, just slow). -""" -function dft_r2hc(x::AbstractVector) - # http://www.fftw.org/fftw3_doc/The-1d-Real_002ddata-DFT.html#The-1d-Real_002ddata-DFT - # http://www.fftw.org/fftw3_doc/The-Halfcomplex_002dformat-DFT.html - # So - # - # * we don't need the imaginary part of y_0 (which will be the first element in y, say, i=1) - # * if n is even, we don't need the imaginary part of y_{n/2} (which would be i = n/2+1) - # - # Now, the order is supposed to be like this (r for real, i for imaginary): - # - # * r_0, r_1, r_2, r_{n/2}, i_{(n+1)/2-1}, ..., i_2, i_1 - # - # But the docs say that they're still using the same old formula, which is: - # - # Y_k = Σ_{j=0}^{n-1} X_j exp(-2*π*i*j*k/n) - # - # (where i is sqrt(-1)). - n = length(x) - xo = OffsetArray(x, 0:n-1) - - y = similar(x) - yo = OffsetArray(y, 0:n-1) - - # Let's do k = 0 first. - yo[0] = sum(xo) - - # Now go from k = 1 to n/2 for the real parts. - T = eltype(x) - for k in 1:n÷2 - yo[k] = zero(T) - for j in 0:n-1 - # yo[k] += xo[j]*exp(-2*pi*sqrt(-1)*j*k/n) - yo[k] += xo[j]*cos(-2*pi*j*k/n) - end - end - - # Now go from 1 to (n+1)/2-1 for the imaginary parts. - for k in 1:(n+1)÷2-1 - yo[n-k] = zero(T) - for j in 0:n-1 - yo[n-k] += xo[j]*sin(-2*pi*j*k/n) - end - end - - return y -end - -""" - dft_hc2r(x::AbstractVector) - -Calculate the inverse discrete Fourier transform of a real-input DFT. - -This is the inverse of `dft_r2hc`, except for a factor of `N`, where `N` is the length of the input (and output), since FFTW computes an "unnormalized" FFT. - -See -http://www.fftw.org/fftw3_doc/The-1d-Real_002ddata-DFT.html#The-1d-Real_002ddata-DFT -and http://www.fftw.org/fftw3_doc/The-Halfcomplex_002dformat-DFT.html for -details. - -Only use this for checking the derivatives of the FFT routines (should work fine, just slow). -""" -function dft_hc2r(x::AbstractVector) - n = length(x) - xo = OffsetArray(x, 0:n-1) - - y = zero(x) - yo = OffsetArray(y, 0:n-1) - - j = 0 - for k in 0:n-1 - yo[k] += xo[j] - end - - # So, I need this loop to get r_1 to r_{n÷2} and i_{(n+1)÷2-1} to i_1. - # Let's say n is even. - # Maybe 8. - # So then n÷2 == 4 and (n+1)÷2-1 == 3. - # So x0 looks like this: - # - # r_0, r_1, r_2, r_3, r_4, i_3, i_2, i_1 - # - # If n is odd, say, 9, then n÷2 == 4 and (n+1)÷2-1 == 4, and x0 looks like this: - # - # r_0, r_1, r_2, r_3, r_4, i_4, i_3, i_2, i_1 - # - for j in 1:(n-1)÷2 - rj = xo[j] - ij = xo[n-j] - for k in 0:n-1 - yo[k] += 2*rj*cos(2*pi*j*k/n) - 2*ij*sin(2*pi*j*k/n) - end - end - - if iseven(n) - # Handle the Nyquist frequency. - j = n÷2 - rj = xo[j] - for k in 0:n-1 - yo[k] += rj*cos(2*pi*j*k/n) - end - end - - return y -end - -@concrete struct RFFTCache - val - jac +struct RFFTCache{TVal,TJac} + val::TVal + jac::TJac end function RFFTCache(::Type{V}, M, N) where {V} diff --git a/src/integrated.jl b/src/integrated.jl new file mode 100644 index 0000000..e0aca7a --- /dev/null +++ b/src/integrated.jl @@ -0,0 +1,23 @@ +""" + OASPL(ap::AbstractPressureTimeHistory) + +Return the overall sound pressure level of a pressure time history. +""" +function OASPL(ap::AbstractPressureTimeHistory) + p = pressure(ap) + n = inputlength(ap) + p_mean = sum(p)/n + msp = sum((p .- p_mean).^2)/n + return 10*log10(msp/p_ref^2) +end + +""" + OASPL(ap::AbstractNarrowbandSpectrum) + +Return the overall sound pressure level of a narrowband spectrum. +""" +function OASPL(sp::AbstractNarrowbandSpectrum) + amp = MSPSpectrumAmplitude(sp) + msp = sum(@view amp[begin+1:end]) + return 10*log10(msp/p_ref^2) +end diff --git a/src/narrowband.jl b/src/narrowband.jl index 88dea82..462dfcd 100644 --- a/src/narrowband.jl +++ b/src/narrowband.jl @@ -76,13 +76,17 @@ Return a vector of times associated with a pressure time history. end """ - AbstractNarrowbandSpectrum{IsEven,Tel} <: AbstractVector{Tel} + AbstractNarrowbandSpectrum{IsEven,IsTonal,Tel} <: AbstractVector{Tel} Supertype for a generic narrowband acoustic metric which will behave as an immutable `AbstractVector` of element type `Tel`. The `IsEven` parameter is a `Bool` indicating if the length of the spectrum is even or not, affecting how the Nyquist frequency is calculated. +`IsTonal` indicates how the acoustic energy is distributed through the frequency bands: + + * `IsTonal == false` means the acoustic energy is assumed to be evenly distributed thoughout each band + * `IsTonal == true` means the acoustic energy is assumed to be concentrated at each band center """ -abstract type AbstractNarrowbandSpectrum{IsEven,Tel} <: AbstractVector{Tel} end +abstract type AbstractNarrowbandSpectrum{IsEven,IsTonal,Tel} <: AbstractVector{Tel} end """ halfcomplex(sm::AbstractNarrowbandSpectrum) @@ -132,6 +136,25 @@ The frequencies are calculated using the `rfftfreq` function in the FFTW.jl pack """ @inline frequency(sm::AbstractNarrowbandSpectrum) = rfftfreq(inputlength(sm), samplerate(sm)) + +""" + frequencystep(sm::AbstractNarrowbandSpectrum) + +Return the frequency step size `Δf` associated with the narrowband spectrum. +""" +@inline function frequencystep(sm::AbstractNarrowbandSpectrum) + m = inputlength(sm) + df = 1/(timestep(sm)*m) + return df +end + +""" + istonal(sm::AbstractNarrowbandSpectrum) + +Return `true` if the spectrum is tonal, `false` otherwise. +""" +@inline istonal(sm::AbstractNarrowbandSpectrum{IsEven,IsTonal}) where {IsEven,IsTonal} = IsTonal + """ PressureTimeHistory(sm::AbstractNarrowbandSpectrum, p=similar(halfcomplex(sm))) @@ -160,32 +183,37 @@ end end """ - PressureSpectrumAmplitude{IsEven,Tel} <: AbstractNarrowbandSpectrum{IsEven,Tel} + PressureSpectrumAmplitude{IsEven,IsTonal,Tel} <: AbstractNarrowbandSpectrum{IsEven,IsTonal,Tel} Representation of acoustic pressure amplitude as a function of narrowband frequency. The `IsEven` parameter is a `Bool` indicating if the length of the spectrum is even or not, affecting how the Nyquist frequency is calculated. +The `IsTonal` `Bool` parameter, if `true`, indicates the pressure spectrum is tonal and thus concentrated at discrete frequencies. +If `false`, the spectrum is assumed to be constant over each frequency band. """ -struct PressureSpectrumAmplitude{IsEven,Tel,Thc,Tdt,Tt0} <: AbstractNarrowbandSpectrum{IsEven,Tel} +struct PressureSpectrumAmplitude{IsEven,IsTonal,Tel,Thc,Tdt,Tt0} <: AbstractNarrowbandSpectrum{IsEven,IsTonal,Tel} hc::Thc dt::Tdt t0::Tt0 - function PressureSpectrumAmplitude{IsEven}(hc, dt, t0) where {IsEven} + function PressureSpectrumAmplitude{IsEven,IsTonal}(hc, dt, t0) where {IsEven,IsTonal} n = length(hc) iseven(n) == IsEven || throw(ArgumentError("IsEven = $(IsEven) is not consistent with length(hc) = $n")) - return new{IsEven, eltype(hc), typeof(hc), typeof(dt), typeof(t0)}(hc, dt, t0) + typeof(IsTonal) === Bool || throw(ArgumentError("typeof(IsTonal) should be Bool")) + return new{IsEven, IsTonal, eltype(hc), typeof(hc), typeof(dt), typeof(t0)}(hc, dt, t0) end end """ - PressureSpectrumAmplitude(hc, dt, t0=zero(dt)) + PressureSpectrumAmplitude(hc, dt, t0=zero(dt), istonal::Bool=false) Construct a narrowband spectrum of the pressure amplitude from the discrete Fourier transform in half-complex format `hc`, time step size `dt`, and initial time `t0`. +The `istonal` `Bool` argument, if `true`, indicates the pressure spectrum is tonal and thus concentrated at discrete frequencies. +If `false`, the spectrum is assumed to be constant over each frequency band. """ -function PressureSpectrumAmplitude(hc, dt, t0=zero(dt)) +function PressureSpectrumAmplitude(hc, dt, t0=zero(dt), istonal::Bool=false) n = length(hc) - return PressureSpectrumAmplitude{iseven(n)}(hc, dt, t0) + return PressureSpectrumAmplitude{iseven(n),istonal}(hc, dt, t0) end """ @@ -193,22 +221,24 @@ end Construct a narrowband spectrum of the pressure amplitude from another narrowband spectrum. """ -PressureSpectrumAmplitude(sm::AbstractNarrowbandSpectrum) = PressureSpectrumAmplitude(halfcomplex(sm), timestep(sm), starttime(sm)) +PressureSpectrumAmplitude(sm::AbstractNarrowbandSpectrum{IsEven,IsTonal}) where {IsEven,IsTonal} = PressureSpectrumAmplitude{IsEven,IsTonal}(halfcomplex(sm), timestep(sm), starttime(sm)) """ - PressureSpectrumAmplitude(pth::AbstractPressureTimeHistory, hc=similar(pressure(pth))) + PressureSpectrumAmplitude(pth::AbstractPressureTimeHistory, istonal::Bool=false, hc=similar(pressure(pth))) Construct a narrowband spectrum of the pressure amplitude from a pressure time history. The optional argument `hc` will be used to store the discrete Fourier transform of the pressure time history, and should have length of `inputlength(pth)`. +The `istonal` `Bool` argument, if `true`, indicates the pressure spectrum is tonal and thus concentrated at discrete frequencies. +If `false`, the spectrum is assumed to be constant over each frequency band. """ -function PressureSpectrumAmplitude(pth::AbstractPressureTimeHistory, hc=similar(pressure(pth))) +function PressureSpectrumAmplitude(pth::AbstractPressureTimeHistory, istonal::Bool=false, hc=similar(pressure(pth))) p = pressure(pth) # Get the FFT of the acoustic pressure. rfft!(hc, p) - return PressureSpectrumAmplitude(hc, timestep(pth), starttime(pth)) + return PressureSpectrumAmplitude(hc, timestep(pth), starttime(pth), istonal) end @inline function Base.getindex(psa::PressureSpectrumAmplitude{false}, i::Int) @@ -238,32 +268,35 @@ end end """ - PressureSpectrumPhase{IsEven,Tel} <: AbstractNarrowbandSpectrum{IsEven,Tel} + PressureSpectrumPhase{IsEven,IsTonal,Tel} <: AbstractNarrowbandSpectrum{IsEven,IsTonal,Tel} Representation of acoustic pressure phase as a function of narrowband frequency. The `IsEven` parameter is a `Bool` indicating if the length of the spectrum is even or not, affecting how the Nyquist frequency is calculated. +The `IsTonal` `Bool` parameter, if `true`, indicates the phase spectrum is tonal and thus concentrated at discrete frequencies. +If `false`, the spectrum is assumed to be constant over each frequency band. """ -struct PressureSpectrumPhase{IsEven,Tel,Thc,Tdt,Tt0} <: AbstractNarrowbandSpectrum{IsEven,Tel} +struct PressureSpectrumPhase{IsEven,IsTonal,Tel,Thc,Tdt,Tt0} <: AbstractNarrowbandSpectrum{IsEven,IsTonal,Tel} hc::Thc dt::Tdt t0::Tt0 - function PressureSpectrumPhase{IsEven}(hc, dt, t0) where {IsEven} + function PressureSpectrumPhase{IsEven,IsTonal}(hc, dt, t0) where {IsEven,IsTonal} n = length(hc) iseven(n) == IsEven || throw(ArgumentError("IsEven = $(IsEven) is not consistent with length(hc) = $n")) - return new{IsEven, eltype(hc), typeof(hc), typeof(dt), typeof(t0)}(hc, dt, t0) + typeof(IsTonal) === Bool || throw(ArgumentError("typeof(IsTonal) should be Bool")) + return new{IsEven, IsTonal, eltype(hc), typeof(hc), typeof(dt), typeof(t0)}(hc, dt, t0) end end """ - PressureSpectrumPhase(hc, dt, t0=zero(dt)) + PressureSpectrumPhase(hc, dt, t0=zero(dt), istonal::Bool=false) Construct a narrowband spectrum of the pressure phase from the discrete Fourier transform in half-complex format `hc`, time step size `dt`, and initial time `t0`. """ -function PressureSpectrumPhase(hc, dt, t0=zero(dt)) +function PressureSpectrumPhase(hc, dt, t0=zero(dt), istonal::Bool=false) n = length(hc) - return PressureSpectrumPhase{iseven(n)}(hc, dt, t0) + return PressureSpectrumPhase{iseven(n),istonal}(hc, dt, t0) end """ @@ -271,22 +304,24 @@ end Construct a narrowband spectrum of the pressure phase from another narrowband spectrum. """ -PressureSpectrumPhase(sm::AbstractNarrowbandSpectrum) = PressureSpectrumPhase(halfcomplex(sm), timestep(sm), starttime(sm)) +PressureSpectrumPhase(sm::AbstractNarrowbandSpectrum{IsEven,IsTonal}) where {IsEven,IsTonal} = PressureSpectrumPhase{IsEven,IsTonal}(halfcomplex(sm), timestep(sm), starttime(sm)) """ - PressureSpectrumPhase(pth::AbstractPressureTimeHistory, hc=similar(pressure(pth))) + PressureSpectrumPhase(pth::AbstractPressureTimeHistory, istonal::Bool=false, hc=similar(pressure(pth))) Construct a narrowband spectrum of the pressure phase from a pressure time history. The optional argument `hc` will be used to store the discrete Fourier transform of the pressure time history, and should have length of `inputlength(pth)`. +The `istonal` `Bool` argument, if `true`, indicates the pressure spectrum is tonal and thus concentrated at discrete frequencies. +If `false`, the spectrum is assumed to be constant over each frequency band. """ -function PressureSpectrumPhase(pth::AbstractPressureTimeHistory, hc=similar(pressure(pth))) +function PressureSpectrumPhase(pth::AbstractPressureTimeHistory, istonal::Bool=false, hc=similar(pressure(pth))) p = pressure(pth) # Get the FFT of the acoustic pressure. rfft!(hc, p) - return PressureSpectrumPhase(hc, timestep(pth), starttime(pth)) + return PressureSpectrumPhase(hc, timestep(pth), starttime(pth), istonal) end @inline function Base.getindex(psp::PressureSpectrumPhase{false}, i::Int) @@ -320,32 +355,37 @@ end end """ - MSPSpectrumAmplitude{IsEven,Tel} <: AbstractNarrowbandSpectrum{IsEven,Tel} + MSPSpectrumAmplitude{IsEven,IsTonal,Tel} <: AbstractNarrowbandSpectrum{IsEven,IsTonal,Tel} Representation of mean-squared pressure amplitude as a function of narrowband frequency. The `IsEven` parameter is a `Bool` indicating if the length of the spectrum is even or not, affecting how the Nyquist frequency is calculated. +The `IsTonal` `Bool` parameter, if `true`, indicates the mean-squared pressure spectrum is tonal and thus concentrated at discrete frequencies. +If `false`, the pressure spectrum is assumed to be constant over each frequency band. """ -struct MSPSpectrumAmplitude{IsEven,Tel,Thc,Tdt,Tt0} <: AbstractNarrowbandSpectrum{IsEven,Tel} +struct MSPSpectrumAmplitude{IsEven,IsTonal,Tel,Thc,Tdt,Tt0} <: AbstractNarrowbandSpectrum{IsEven,IsTonal,Tel} hc::Thc dt::Tdt t0::Tt0 - function MSPSpectrumAmplitude{IsEven}(hc, dt, t0) where {IsEven} + function MSPSpectrumAmplitude{IsEven,IsTonal}(hc, dt, t0) where {IsEven,IsTonal} n = length(hc) iseven(n) == IsEven || throw(ArgumentError("IsEven = $(IsEven) is not consistent with length(hc) = $n")) - return new{IsEven, eltype(hc), typeof(hc), typeof(dt), typeof(t0)}(hc, dt, t0) + typeof(IsTonal) === Bool || throw(ArgumentError("typeof(IsTonal) should be Bool")) + return new{IsEven, IsTonal, eltype(hc), typeof(hc), typeof(dt), typeof(t0)}(hc, dt, t0) end end """ - MSPSpectrumAmplitude(hc, dt, t0=zero(dt)) + MSPSpectrumAmplitude(hc, dt, t0=zero(dt), istonal::Bool=false) Construct a narrowband spectrum of the mean-squared pressure amplitude from the discrete Fourier transform in half-complex format `hc`, time step size `dt`, and initial time `t0`. +The `istonal` `Bool` argument, if `true`, indicates the pressure spectrum is tonal and thus concentrated at discrete frequencies. +If `false`, the spectrum is assumed to be constant over each frequency band. """ -function MSPSpectrumAmplitude(hc, dt, t0=zero(dt)) +function MSPSpectrumAmplitude(hc, dt, t0=zero(dt), istonal::Bool=false) n = length(hc) - return MSPSpectrumAmplitude{iseven(n)}(hc, dt, t0) + return MSPSpectrumAmplitude{iseven(n),istonal}(hc, dt, t0) end """ @@ -353,22 +393,24 @@ end Construct a narrowband spectrum of the mean-squared pressure amplitude from another narrowband spectrum. """ -MSPSpectrumAmplitude(sm::AbstractNarrowbandSpectrum) = MSPSpectrumAmplitude(halfcomplex(sm), timestep(sm), starttime(sm)) +MSPSpectrumAmplitude(sm::AbstractNarrowbandSpectrum{IsEven,IsTonal}) where {IsEven,IsTonal} = MSPSpectrumAmplitude{IsEven,IsTonal}(halfcomplex(sm), timestep(sm), starttime(sm)) """ - MSPSpectrumAmplitude(pth::AbstractPressureTimeHistory, hc=similar(pressure(pth))) + MSPSpectrumAmplitude(pth::AbstractPressureTimeHistory, istonal::Bool=false, hc=similar(pressure(pth))) Construct a narrowband spectrum of the mean-squared pressure amplitude from a pressure time history. The optional argument `hc` will be used to store the discrete Fourier transform of the pressure time history, and should have length of `inputlength(pth)`. +The `istonal` `Bool` argument, if `true`, indicates the pressure spectrum is tonal and thus concentrated at discrete frequencies. +If `false`, the spectrum is assumed to be constant over each frequency band. """ -function MSPSpectrumAmplitude(pth::AbstractPressureTimeHistory, hc=similar(pressure(pth))) +function MSPSpectrumAmplitude(pth::AbstractPressureTimeHistory, istonal::Bool=false, hc=similar(pressure(pth))) p = pressure(pth) # Get the FFT of the acoustic pressure. rfft!(hc, p) - return MSPSpectrumAmplitude(hc, timestep(pth), starttime(pth)) + return MSPSpectrumAmplitude(hc, timestep(pth), starttime(pth), istonal) end @inline function Base.getindex(psa::MSPSpectrumAmplitude{false}, i::Int) @@ -405,13 +447,14 @@ Alias for `PressureSpectrumPhase`. const MSPSpectrumPhase = PressureSpectrumPhase """ - PowerSpectralDensityAmplitude{IsEven,Tel} <: AbstractNarrowbandSpectrum{IsEven,Tel} + PowerSpectralDensityAmplitude{IsEven,Tel} <: AbstractNarrowbandSpectrum{IsEven,false,Tel} Representation of acoustic power spectral density amplitude as a function of narrowband frequency. The `IsEven` parameter is a `Bool` indicating if the length of the spectrum is even or not, affecting how the Nyquist frequency is calculated. +As the power spectral density is not well-defined for tones, the `IsTonal` parameter is always `false`. """ -struct PowerSpectralDensityAmplitude{IsEven,Tel,Thc,Tdt,Tt0} <: AbstractNarrowbandSpectrum{IsEven,Tel} +struct PowerSpectralDensityAmplitude{IsEven,Tel,Thc,Tdt,Tt0} <: AbstractNarrowbandSpectrum{IsEven,false,Tel} hc::Thc dt::Tdt t0::Tt0 @@ -434,14 +477,15 @@ function PowerSpectralDensityAmplitude(hc, dt, t0=zero(dt)) end """ - PressureSpectrumAmplitude(sm::AbstractNarrowbandSpectrum) + PowerSpectralDensityAmplitude(sm::AbstractNarrowbandSpectrum) Construct a narrowband spectrum of the power spectral density amplitude from another narrowband spectrum. """ -PowerSpectralDensityAmplitude(sm::AbstractNarrowbandSpectrum) = PowerSpectralDensityAmplitude(halfcomplex(sm), timestep(sm), starttime(sm)) +PowerSpectralDensityAmplitude(sm::AbstractNarrowbandSpectrum{IsEven,false}) where {IsEven} = PowerSpectralDensityAmplitude(halfcomplex(sm), timestep(sm), starttime(sm)) +PowerSpectralDensityAmplitude(sm::AbstractNarrowbandSpectrum{IsEven,true}) where {IsEven} = throw(ArgumentError("IsTonal == true parameter cannot be used with PowerSpectralDensityAmplitude type")) """ - PressureSpectrumAmplitude(pth::AbstractPressureTimeHistory, hc=similar(pressure(pth))) + PowerSpectralDensityAmplitude(pth::AbstractPressureTimeHistory, hc=similar(pressure(pth))) Construct a narrowband spectrum of the power spectral density amplitude from a pressure time history. @@ -459,7 +503,7 @@ end @inline function Base.getindex(psa::PowerSpectralDensityAmplitude{false}, i::Int) @boundscheck checkbounds(psa, i) m = inputlength(psa) - df = 1/(timestep(psa)*m) + df = frequencystep(psa) if i == 1 @inbounds hc_real = psa.hc[i]/m return hc_real^2/df @@ -473,7 +517,7 @@ end @inline function Base.getindex(psa::PowerSpectralDensityAmplitude{true}, i::Int) @boundscheck checkbounds(psa, i) m = inputlength(psa) - df = 1/(timestep(psa)*m) + df = frequencystep(psa) if i == 1 || i == length(psa) @inbounds hc_real = psa.hc[i]/m return hc_real^2/df @@ -490,27 +534,3 @@ end Alias for `PressureSpectrumPhase`. """ const PowerSpectralDensityPhase = PressureSpectrumPhase - -""" - OASPL(ap::AbstractPressureTimeHistory) - -Return the overall sound pressure level of a pressure time history. -""" -function OASPL(ap::AbstractPressureTimeHistory) - p = pressure(ap) - n = inputlength(ap) - p_mean = sum(p)/n - msp = sum((p .- p_mean).^2)/n - return 10*log10(msp/p_ref^2) -end - -""" - OASPL(ap::AbstractNarrowbandSpectrum) - -Return the overall sound pressure level of a narrowband spectrum. -""" -function OASPL(sp::AbstractNarrowbandSpectrum) - amp = MSPSpectrumAmplitude(sp) - msp = sum(@view amp[begin+1:end]) - return 10*log10(msp/p_ref^2) -end diff --git a/src/proportional_bands.jl b/src/proportional_bands.jl index 147e2b7..bdde455 100644 --- a/src/proportional_bands.jl +++ b/src/proportional_bands.jl @@ -11,9 +11,96 @@ The `LCU` parameter can take one of three values: """ abstract type AbstractProportionalBands{NO,LCU,TF} <: AbstractVector{TF} end +""" + octave_fraction(bands::AbstractProportionalBands{NO}) where {NO} + +Return `NO`, the "octave fraction," e.g. `1` for octave bands, `3` for third-octave, `12` for twelfth-octave. +""" +octave_fraction(::AbstractProportionalBands{NO}) where {NO} = NO octave_fraction(::Type{<:AbstractProportionalBands{NO}}) where {NO} = NO + +""" + lower_center_upper(bands::AbstractProportionalBands{NO,LCU,TF}) where {NO,LCU,TF} + +Return `LCU`, which can be either `:lower`, `:center`, `:upper`, indicating if `bands` represents the lower edges, centers, or upper edges of proportional bands, respectively. +""" +lower_center_upper(bands::AbstractProportionalBands{NO,LCU,TF}) where {NO,LCU,TF} = lower_center_upper(typeof(bands)) lower_center_upper(::Type{<:AbstractProportionalBands{NO,LCU,TF}}) where {NO,LCU,TF} = LCU +""" + freq_scaler(bands::AbstractProportionalBands) + +Return the factor each "standard" frequency band is scaled by. + +For example, the approximate octave center bands include 1000 Hz, 2000 Hz, and 4000 Hz. +If `freq_scaler(bands) == 1.0`, then these frequencies would be unchanged. +If `freq_scaler(bands) == 1.5`, then `bands` would include 1500 Hz, 3000 Hz, and 6000 Hz instead. +If `freq_scaler(bands) == 0.5`, then `bands` would include 500 Hz, 1000 Hz, and 2000 Hz in place of 1000 Hz, 2000 Hz, and 4000 Hz. +""" +@inline freq_scaler(bands::AbstractProportionalBands) = bands.scaler + +""" + band_start(bands::AbstractProportionalBands) + +Return the standard band index number for the first band in `bands`. + +For example, it happens that the approximate octave center bands includes 1000 Hz, and that particular band is numbered `10`. +So if the first band contained in `bands` happens to be 1000 Hz (and `freq_scaler(bands) == 1.0`), then `band_start(bands) == 10`. +Not particularly useful to a user. +""" +@inline band_start(bands::AbstractProportionalBands) = bands.bstart + +""" + band_end(bands::AbstractProportionalBands) + +Return the standard band index number for the last band in `bands`. + +For example, it happens that the approximate octave center bands includes 1000 Hz, and that particular band is numbered `10`. +So if the last band contained in `bands` happens to be 1000 Hz (and `freq_scaler(bands) == 1.0`), then `band_end(bands) == 10`. +Not particularly useful to a user. +""" +@inline band_end(bands::AbstractProportionalBands) = bands.bend + +@inline function Base.size(bands::AbstractProportionalBands) + return (band_end(bands) - band_start(bands) + 1,) +end + +""" + lower_bands(TBands::Type{<:AbstractProportionalBands{NO}}, fstart::TF, fend::TF, scaler=1) where {NO,TF} + +Construct and return the lower edges of the proportional bands `TBands`, scaled by `scaler`, that would fully encompass a frequency range beginning with `fstart` and ending with `fend`. +""" +function lower_bands(TBands::Type{<:AbstractProportionalBands{NO}}, fstart::TF, fend::TF, scaler=1) where {NO,TF} + return TBands{:lower}(fstart, fend, scaler) +end + +""" + upper_bands(TBands::Type{<:AbstractProportionalBands{NO}}, fstart::TF, fend::TF, scaler=1) where {NO,TF} + +Construct and return the upper edges of the proportional bands `TBands`, scaled by `scaler`, that would fully encompass a frequency range beginning with `fstart` and ending with `fend`. +""" +function upper_bands(TBands::Type{<:AbstractProportionalBands{NO}}, fstart::TF, fend::TF, scaler=1) where {NO,TF} + return TBands{:upper}(fstart, fend, scaler) +end + +""" + center_bands(TBands::Type{<:AbstractProportionalBands{NO}}, fstart::TF, fend::TF, scaler=1) where {NO,TF} + +Construct and return the centers of the proportional bands `TBands`, scaled by `scaler`, that would fully encompass a frequency range beginning with `fstart` and ending with `fend`. +""" +function center_bands(TBands::Type{<:AbstractProportionalBands{NO}}, fstart::TF, fend::TF, scaler=1) where {NO,TF} + return TBands{:center}(fstart, fend, scaler) +end + +""" + cband_number(bands::AbstractProportionalBands, fc) + +Return the standard band index number of the band with center frequency `fc` for proportional bands `bands`. + +For example, if `bands` is a subtype of `ApproximateOctaveBands` and `freq_scaler(bands) == 1.0`, then `cband_number(bands, 1000.0) == 10`. +""" +cband_number(bands::AbstractProportionalBands, fc) = cband_number(typeof(bands), fc, freq_scaler(bands)) + const f0_exact = 1000 const fmin_exact = 1 @@ -30,63 +117,69 @@ The `LCU` parameter can take one of three values: """ ExactProportionalBands -""" - ExactProportionalBands{NO,LCU}(TF=Float64, bstart::Int, bend::Int) - -Construct an `ExactProportionalBands` with `eltype` `TF` encomposing band numbers from `bstart` to `bend`. -""" struct ExactProportionalBands{NO,LCU,TF} <: AbstractProportionalBands{NO,LCU,TF} bstart::Int bend::Int f0::TF - function ExactProportionalBands{NO,LCU,TF}(bstart::Int, bend::Int) where {NO,LCU,TF} - NO > 0 || throw(ArgumentError("Octave band fraction NO = $NO should be greater than 0")) + scaler::TF + function ExactProportionalBands{NO,LCU,TF}(bstart::Int, bend::Int, scaler=1) where {NO,LCU,TF} + NO > 0 || throw(ArgumentError("Octave band fraction NO must be greater than 0")) LCU in (:lower, :center, :upper) || throw(ArgumentError("LCU must be one of :lower, :center, :upper")) bend >= bstart || throw(ArgumentError("bend should be greater than or equal to bstart")) - return new{NO,LCU,TF}(bstart, bend, TF(f0_exact)) - end - function ExactProportionalBands{NO,LCU}(TF, bstart::Int, bend::Int) where {NO,LCU} - return ExactProportionalBands{NO,LCU,TF}(bstart, bend) + scaler > 0 || throw(ArgumentError("non-positive scaler argument not supported")) + return new{NO,LCU,TF}(bstart, bend, TF(f0_exact), TF(scaler)) end end -@inline band_start(bands::AbstractProportionalBands) = bands.bstart -@inline band_end(bands::AbstractProportionalBands) = bands.bend -@inline function Base.size(bands::AbstractProportionalBands) - return (band_end(bands) - band_start(bands) + 1,) -end +""" + ExactProportionalBands{NO,LCU}(TF=Float64, bstart::Int, bend::Int, scaler=1) -function bands_lower(TBands::Type{<:AbstractProportionalBands{NO}}, fstart::TF, fend::TF) where {NO,TF} - return TBands{:lower}(fstart, fend) -end +Construct an `ExactProportionalBands` with `eltype` `TF` encomposing band index numbers from `bstart` to `bend`. -function bands_upper(TBands::Type{<:AbstractProportionalBands{NO}}, fstart::TF, fend::TF) where {NO,TF} - return TBands{:upper}(fstart, fend) +The "standard" band frequencies will be scaled by `scaler`, e.g. if `scaler = 0.5` then what would normally be the `1000 Hz` frequency will be `500 Hz`, etc.. +""" +function ExactProportionalBands{NO,LCU}(TF::Type, bstart::Int, bend::Int, scalar=1) where {NO,LCU} + return ExactProportionalBands{NO,LCU,TF}(bstart, bend, scalar) end - -function bands_center(TBands::Type{<:AbstractProportionalBands{NO}}, fstart::TF, fend::TF) where {NO,TF} - return TBands{:center}(fstart, fend) +function ExactProportionalBands{NO,LCU}(bstart::Int, bend::Int, scaler=1) where {NO,LCU} + return ExactProportionalBands{NO,LCU}(Float64, bstart, bend, scaler) end -ExactProportionalBands{NO,LCU}(bstart::Int, bend::Int) where {NO,LCU} = ExactProportionalBands{NO,LCU}(Float64, bstart, bend) +@inline band_exact_lower_limit(NO, fl, scaler) = floor(Int, 1/2 + NO*log2(fl/(f0_exact*scaler)) + 10*NO) +@inline band_exact_upper_limit(NO, fu, scaler) = ceil(Int, -1/2 + NO*log2(fu/(f0_exact*scaler)) + 10*NO) -@inline band_exact_lower_limit(NO, fl) = floor(Int, 1/2 + NO*log2(fl/f0_exact) + 10*NO) -@inline band_exact_upper_limit(NO, fu) = ceil(Int, -1/2 + NO*log2(fu/f0_exact) + 10*NO) +function _cband_exact(NO, fc, scaler) + # f = 2^((b - 10*NO)/NO)*f0 + # f/f0 = 2^((b - 10*NO)/NO) + # log2(f/f0) = log2(2^((b - 10*NO)/NO)) + # log2(f/f0) = ((b - 10*NO)/NO) + # log2(f/f0)*NO = b - 10*NO + # log2(f/f0)*NO + 10*NO = b + # b = log2(f/f0)*NO + 10*NO -""" - ExactProportionalBands{NO,LCU}(fstart::TF, fend::TF) + # Get the band number from a center band frequency `fc`. + log2_fc_over_f0_exact_NO = log2(fc/(f0_exact*scaler))*NO -Construct an `ExactProportionalBands` with `eltype` `TF` encomposing the bands needed to completly extend over minimum frequency `fstart` and maximum frequency `fend`. -""" -ExactProportionalBands{NO,LCU}(fstart::TF, fend::TF) where {NO,LCU,TF} = ExactProportionalBands{NO,LCU,TF}(fstart, fend) -ExactProportionalBands{NO,LCU,TF}(fstart::TF, fend::TF) where {NO,LCU,TF} = ExactProportionalBands{NO,LCU,TF}(band_exact_lower_limit(NO, fstart), band_exact_upper_limit(NO, fend)) + # Check that the result will be very close to an integer. + rounded = round(Int, log2_fc_over_f0_exact_NO) + tol = 10*eps(fc) + abs_cs_safe(log2_fc_over_f0_exact_NO - rounded) < tol || throw(ArgumentError("fc does not correspond to a center-band frequency")) + + b = rounded + 10*NO + return b +end + +function cband_number(::Type{<:ExactProportionalBands{NO}}, fc, scaler) where {NO} + return _cband_exact(NO, fc, scaler) +end """ - Base.getindex(bands::ExactProportionalBands{NO,LCU}, i::Int) where {NO,LCU} + ExactProportionalBands{NO,LCU}(fstart::TF, fend::TF, scaler) -Return the lower, center, or upper frequency (depending on the value of `LCU`) associated with the `i`-th proportional band frequency covered by `bands`. +Construct an `ExactProportionalBands` with `eltype` `TF`, scaled by `scaler`, encomposing the bands needed to completely extend over minimum frequency `fstart` and maximum frequency `fend`. """ -Base.getindex(bands::ExactProportionalBands, i::Int) +ExactProportionalBands{NO,LCU}(fstart::TF, fend::TF, scaler=1) where {NO,LCU,TF} = ExactProportionalBands{NO,LCU,TF}(fstart, fend, scaler) +ExactProportionalBands{NO,LCU,TF}(fstart::TF, fend::TF, scaler=1) where {NO,LCU,TF} = ExactProportionalBands{NO,LCU,TF}(band_exact_lower_limit(NO, fstart, scaler), band_exact_upper_limit(NO, fend, scaler), scaler) @inline function Base.getindex(bands::ExactProportionalBands{NO,:center}, i::Int) where {NO} @boundscheck checkbounds(bands, i) @@ -100,7 +193,7 @@ Base.getindex(bands::ExactProportionalBands, i::Int) # where f_0 is the reference frequency, 1000 Hz. # OK, so. # 2^((b - 10*NO)/NO)*f_c - return 2^((b - 10*NO)/NO)*bands.f0 + return 2^((b - 10*NO)/NO)*(bands.f0*freq_scaler(bands)) end @inline function Base.getindex(bands::ExactProportionalBands{NO,:lower}, i::Int) where {NO} @@ -108,7 +201,7 @@ end b = bands.bstart + (i - 1) # return 2^((b - 10*NO)/NO)*(2^(-1/(2*NO)))*bands.f0 # return 2^(2*(b - 10*NO)/(2*NO))*(2^(-1/(2*NO)))*bands.f0 - return 2^((2*(b - 10*NO) - 1)/(2*NO))*bands.f0 + return 2^((2*(b - 10*NO) - 1)/(2*NO))*(bands.f0*freq_scaler(bands)) end @inline function Base.getindex(bands::ExactProportionalBands{NO,:upper}, i::Int) where {NO} @@ -116,54 +209,71 @@ end b = bands.bstart + (i - 1) # return 2^((b - 10*NO)/NO)*(2^(1/(2*NO)))*bands.f0 # return 2^(2*(b - 10*NO)/(2*NO))*(2^(1/(2*NO)))*bands.f0 - return 2^((2*(b - 10*NO) + 1)/(2*NO))*bands.f0 + return 2^((2*(b - 10*NO) + 1)/(2*NO))*(bands.f0*freq_scaler(bands)) end """ ExactOctaveCenterBands{TF} -Alias for ExactProportionalBands{1,:center,TF} +Alias for `ExactProportionalBands{1,:center,TF}` """ const ExactOctaveCenterBands{TF} = ExactProportionalBands{1,:center,TF} """ ExactThirdOctaveCenterBands{TF} -Alias for ExactProportionalBands{3,:center,TF} +Alias for `ExactProportionalBands{3,:center,TF}` """ const ExactThirdOctaveCenterBands{TF} = ExactProportionalBands{3,:center,TF} """ ExactOctaveLowerBands{TF} -Alias for ExactProportionalBands{1,:lower,TF} +Alias for `ExactProportionalBands{1,:lower,TF}` """ const ExactOctaveLowerBands{TF} = ExactProportionalBands{1,:lower,TF} """ ExactThirdOctaveLowerBands{TF} -Alias for ExactProportionalBands{3,:lower,TF} +Alias for `ExactProportionalBands{3,:lower,TF}` """ const ExactThirdOctaveLowerBands{TF} = ExactProportionalBands{3,:lower,TF} """ ExactOctaveUpperBands{TF} -Alias for ExactProportionalBands{1,:upper,TF} +Alias for `ExactProportionalBands{1,:upper,TF}` """ const ExactOctaveUpperBands{TF} = ExactProportionalBands{1,:upper,TF} """ ExactThirdOctaveUpperBands{TF} -Alias for ExactProportionalBands{3,:upper,TF} +Alias for `ExactProportionalBands{3,:upper,TF}` """ const ExactThirdOctaveUpperBands{TF} = ExactProportionalBands{3,:upper,TF} -lower_bands(bands::ExactProportionalBands{NO,LCU,TF}) where {NO,LCU,TF} = ExactProportionalBands{NO,:lower,TF}(bands.bstart, bands.bend) -center_bands(bands::ExactProportionalBands{NO,LCU,TF}) where {NO,LCU,TF} = ExactProportionalBands{NO,:center,TF}(bands.bstart, bands.bend) -upper_bands(bands::ExactProportionalBands{NO,LCU,TF}) where {NO,LCU,TF} = ExactProportionalBands{NO,:upper,TF}(bands.bstart, bands.bend) +""" + lower_bands(bands::ExactProportionalBands{NO,LCU,TF}, scaler=freq_scaler(bands)) where {NO,TF} + +Construct and return the lower edges of the proportional bands `bands` scaled by `scaler`. +""" +lower_bands(bands::ExactProportionalBands{NO,LCU,TF}, scaler=freq_scaler(bands)) where {NO,LCU,TF} = ExactProportionalBands{NO,:lower,TF}(band_start(bands), band_end(bands), scaler) + +""" + center_bands(bands::ExactProportionalBands{NO,LCU,TF}, scaler=freq_scaler(bands)) where {NO,TF} + +Construct and return the centers of the proportional bands `bands` scaled by `scaler`. +""" +center_bands(bands::ExactProportionalBands{NO,LCU,TF}, scaler=freq_scaler(bands)) where {NO,LCU,TF} = ExactProportionalBands{NO,:center,TF}(band_start(bands), band_end(bands), scaler) + +""" + upper_bands(bands::ExactProportionalBands{NO,LCU,TF}, scaler=freq_scaler(bands)) where {NO,TF} + +Construct and return the upper edges of the proportional bands `bands` scaled by `scaler`. +""" +upper_bands(bands::ExactProportionalBands{NO,LCU,TF}, scaler=freq_scaler(bands)) where {NO,LCU,TF} = ExactProportionalBands{NO,:upper,TF}(band_start(bands), band_end(bands), scaler) const approx_3rd_octave_cbands_pattern = [1.0, 1.25, 1.6, 2.0, 2.5, 3.15, 4.0, 5.0, 6.3, 8.0] const approx_3rd_octave_lbands_pattern = [0.9, 1.12, 1.4, 1.8, 2.24, 2.8, 3.35, 4.5, 5.6, 7.1] @@ -183,39 +293,36 @@ The `LCU` parameter can take one of three values: struct ApproximateThirdOctaveBands{LCU,TF} <: AbstractProportionalBands{3,LCU,TF} bstart::Int bend::Int + scaler::TF - function ApproximateThirdOctaveBands{LCU,TF}(bstart::Int, bend::Int) where {LCU, TF} + function ApproximateThirdOctaveBands{LCU,TF}(bstart::Int, bend::Int, scaler=1) where {LCU, TF} LCU in (:lower, :center, :upper) || throw(ArgumentError("LCU must be one of :lower, :center, :upper")) bend >= bstart || throw(ArgumentError("bend should be greater than or equal to bstart")) - return new{LCU,TF}(bstart, bend) - end - function ApproximateThirdOctaveBands{LCU}(TF, bstart::Int, bend::Int) where {LCU} - return ApproximateThirdOctaveBands{LCU,TF}(bstart, bend) + scaler > 0 || throw(ArgumentError("non-positive scaler argument not supported")) + return new{LCU,TF}(bstart, bend, TF(scaler)) end end """ - ApproximateThirdOctaveBands{LCU,TF}(bstart::Int, bend::Int) + ApproximateThirdOctaveBands{LCU}(TF=Float64, bstart::Int, bend::Int, scaler=1) -Construct an `ApproximateThirdOctaveBands` with `eltype` `TF` encomposing band numbers from `bstart` to `bend`. +Construct an `ApproximateThirdOctaveBands` with `eltype` `TF` encomposing band index numbers from `bstart` to `bend`. -`TF` defaults to `Float64`. +The "standard" band frequencies will be scaled by `scaler`, e.g. if `scaler = 0.5` then what would normally be the `1000 Hz` frequency will be `500 Hz`, etc.. """ -ApproximateThirdOctaveBands{LCU}(bstart::Int, bend::Int) where {LCU} = ApproximateThirdOctaveBands{LCU,Float64}(bstart, bend) - -""" - Base.getindex(bands::ApproximateThirdOctaveBands{LCU}, i::Int) where {LCU} - -Return the lower, center, or upper frequency (depending on the value of `LCU`) associated with the `i`-th proportional band frequency covered by `bands`. -""" -Base.getindex(bands::ApproximateThirdOctaveBands, i::Int) +function ApproximateThirdOctaveBands{LCU}(TF::Type, bstart::Int, bend::Int, scaler=1) where {LCU} + return ApproximateThirdOctaveBands{LCU,TF}(bstart, bend, scaler) +end +function ApproximateThirdOctaveBands{LCU}(bstart::Int, bend::Int, scaler=1) where {LCU} + return ApproximateThirdOctaveBands{LCU}(Float64, bstart, bend, scaler) +end @inline function Base.getindex(bands::ApproximateThirdOctaveBands{:center,TF}, i::Int) where {TF} @boundscheck checkbounds(bands, i) j = bands.bstart + i - 1 factor10, b0 = divrem(j, 10, RoundDown) b = b0 + 1 - return approx_3rd_octave_cbands_pattern[b]*TF(10)^factor10 + return freq_scaler(bands)*approx_3rd_octave_cbands_pattern[b]*TF(10)^factor10 end @inline function Base.getindex(bands::ApproximateThirdOctaveBands{:lower,TF}, i::Int) where {TF} @@ -223,7 +330,7 @@ end j = bands.bstart + i - 1 factor10, b0 = divrem(j, 10, RoundDown) b = b0 + 1 - return approx_3rd_octave_lbands_pattern[b]*TF(10)^factor10 + return freq_scaler(bands)*approx_3rd_octave_lbands_pattern[b]*TF(10)^factor10 end @inline function Base.getindex(bands::ApproximateThirdOctaveBands{:upper,TF}, i::Int) where {TF} @@ -231,12 +338,15 @@ end j = bands.bstart + i - 1 factor10, b0 = divrem(j, 10, RoundDown) b = b0 + 1 - return approx_3rd_octave_ubands_pattern[b]*TF(10)^factor10 + return freq_scaler(bands)*approx_3rd_octave_ubands_pattern[b]*TF(10)^factor10 end -@inline function band_approx_3rd_octave_lower_limit(fl::TF) where {TF} - factor10 = floor(Int, log10(fl/approx_3rd_octave_lbands_pattern[1])) - i = searchsortedfirst(approx_3rd_octave_lbands_pattern, fl; lt=(lband, f)->isless(lband*TF(10)^factor10, f)) +@inline function band_approx_3rd_octave_lower_limit(fl::TF, scaler) where {TF} + # For the `scaler`, I've been thinking about always leaving the input frequency (here `fl`) alone and modifying the standard bands (here `approx_3rd_octave_lbands_pattern`). + # But then that would involve multiplying all of `approx_3rd_octave_lbands_pattern`... + # Or maybe not. + factor10 = floor(Int, log10(fl/(scaler*approx_3rd_octave_lbands_pattern[1]))) + i = searchsortedfirst(approx_3rd_octave_lbands_pattern, fl; lt=(lband, f)->isless(scaler*lband*TF(10)^factor10, f)) # - 2 because # # * -1 for searchsortedfirst giving us the first index in approx_3rd_octave_lbands_pattern that is greater than fl, and we want the band before that @@ -244,33 +354,95 @@ end return (i - 2) + factor10*10 end -@inline function band_approx_3rd_octave_upper_limit(fu::TF) where {TF} - factor10 = floor(Int, log10(fu/approx_3rd_octave_lbands_pattern[1])) - i = searchsortedfirst(approx_3rd_octave_ubands_pattern, fu; lt=(uband, f)->isless(uband*TF(10)^factor10, f)) +@inline function band_approx_3rd_octave_upper_limit(fu::TF, scaler) where {TF} + factor10 = floor(Int, log10(fu/(scaler*approx_3rd_octave_lbands_pattern[1]))) + i = searchsortedfirst(approx_3rd_octave_ubands_pattern, fu; lt=(uband, f)->isless(scaler*uband*TF(10)^factor10, f)) # - 1 because # # * -1 because the array approx_3rd_octave_lbands_pattern is 1-based, but the third-octave band pattern band numbers are 0-based (centerband 1.0 Hz is band number 0, etc..) return (i - 1) + factor10*10 end +function cband_approx_3rd_octave(fc, scaler) + fc_scaled = fc/scaler + frac, factor10 = modf(log10(fc_scaled)) + # if (frac < -eps(frac)) + # frac += 1 + # factor10 -= 1 + # end + adj = ifelse(frac < -eps(frac), 1, 0) + frac += adj + factor10 -= adj + cband_pattern_entry = 10^frac + tol_shift = 0.001 + b = searchsortedfirst(approx_3rd_octave_cbands_pattern, cband_pattern_entry-tol_shift) + tol_compare = 100*eps(approx_3rd_octave_cbands_pattern[b]) + abs_cs_safe(approx_3rd_octave_cbands_pattern[b] - cband_pattern_entry) < tol_compare || throw(ArgumentError("frequency fc does not correspond to an approximate 3rd-octave center band")) + b0 = b - 1 + j = 10*Int(factor10) + b0 + return j +end + +function cband_number(::Type{<:ApproximateThirdOctaveBands}, fc, scaler) + return cband_approx_3rd_octave(fc, scaler) +end + +""" + ApproximateThirdOctaveBands{LCU}(fstart::TF, fend::TF, scaler=1) + +Construct an `ApproximateThirdOctaveBands` with `eltype` `TF`, scaled by `scaler`, encomposing the bands needed to completely extend over minimum frequency `fstart` and maximum frequency `fend`. """ - ApproximateThirdOctaveBands{LCU}(fstart::TF, fend::TF) +ApproximateThirdOctaveBands{LCU}(fstart::TF, fend::TF, scaler=1) where {LCU,TF} = ApproximateThirdOctaveBands{LCU,TF}(fstart, fend, scaler) +ApproximateThirdOctaveBands{LCU,TF}(fstart::TF, fend::TF, scaler=1) where {LCU,TF} = ApproximateThirdOctaveBands{LCU,TF}(band_approx_3rd_octave_lower_limit(fstart, scaler), band_approx_3rd_octave_upper_limit(fend, scaler), scaler) -Construct an `ApproximateThirdOctaveBands` with `eltype` `TF` encomposing the bands needed to completly extend over minimum frequency `fstart` and maximum frequency `fend`. """ -ApproximateThirdOctaveBands{LCU}(fstart::TF, fend::TF) where {LCU,TF} = ApproximateThirdOctaveBands{LCU,TF}(fstart, fend) -ApproximateThirdOctaveBands{LCU,TF}(fstart::TF, fend::TF) where {LCU,TF} = ApproximateThirdOctaveBands{LCU,TF}(band_approx_3rd_octave_lower_limit(fstart), band_approx_3rd_octave_upper_limit(fend)) + ApproximateThirdOctaveCenterBands{TF} +Alias for `ApproximateThirdOctaveBands{:center,TF}` +""" const ApproximateThirdOctaveCenterBands{TF} = ApproximateThirdOctaveBands{:center,TF} + +""" + ApproximateThirdOctaveLowerBands{TF} + +Alias for `ApproximateThirdOctaveBands{:lower,TF}` +""" const ApproximateThirdOctaveLowerBands{TF} = ApproximateThirdOctaveBands{:lower,TF} + +""" + ApproximateThirdOctaveUpperBands{TF} + +Alias for `ApproximateThirdOctaveBands{:upper,TF}` +""" const ApproximateThirdOctaveUpperBands{TF} = ApproximateThirdOctaveBands{:upper,TF} +""" + lower_bands(bands::ApproximateThirdOctaveBands{LCU,TF}, scaler=freq_scaler(bands)) where {LCU,TF} + +Construct and return the lower edges of the proportional bands `bands` scaled by `scaler`. +""" +lower_bands(bands::ApproximateThirdOctaveBands{LCU,TF}, scaler=freq_scaler(bands)) where {LCU,TF} = ApproximateThirdOctaveBands{:lower,TF}(band_start(bands), band_end(bands), scaler) + +""" + center_bands(bands::ApproximateThirdOctaveBands{LCU,TF}, scaler=freq_scaler(bands)) where {LCU,TF} + +Construct and return the centers of the proportional bands `bands` scaled by `scaler`. +""" +center_bands(bands::ApproximateThirdOctaveBands{LCU,TF}, scaler=freq_scaler(bands)) where {LCU,TF} = ApproximateThirdOctaveBands{:center,TF}(band_start(bands), band_end(bands), scaler) + +""" + upper_bands(bands::ApproximateThirdOctaveBands{LCU,TF}, scaler=freq_scaler(bands)) where {LCU,TF} + +Construct and return the upper edges of the proportional bands `bands` scaled by `scaler`. +""" +upper_bands(bands::ApproximateThirdOctaveBands{LCU,TF}, scaler=freq_scaler(bands)) where {LCU,TF} = ApproximateThirdOctaveBands{:upper,TF}(band_start(bands), band_end(bands), scaler) + const approx_octave_cbands_pattern = [1.0, 2.0, 4.0, 8.0, 16.0, 31.5, 63.0, 125.0, 250.0, 500.0] const approx_octave_lbands_pattern = [0.71, 1.42, 2.84, 5.68, 11.0, 22.0, 44.0, 88.0, 177.0, 355.0] const approx_octave_ubands_pattern = [1.42, 2.84, 5.68, 11.0, 22.0, 44.0, 88.0, 177.0, 355.0, 710.0] """ - ApproximateOctaveBands{LCU,TF} <: AbstractProportionalBands{3,LCU,TF} + ApproximateOctaveBands{LCU,TF} <: AbstractProportionalBands{1,LCU,TF} Representation of the approximate octave proportional frequency bands with `eltype` `TF`. @@ -283,39 +455,36 @@ The `LCU` parameter can take one of three values: struct ApproximateOctaveBands{LCU,TF} <: AbstractProportionalBands{1,LCU,TF} bstart::Int bend::Int + scaler::TF - function ApproximateOctaveBands{LCU,TF}(bstart::Int, bend::Int) where {LCU, TF} + function ApproximateOctaveBands{LCU,TF}(bstart::Int, bend::Int, scaler=1) where {LCU, TF} LCU in (:lower, :center, :upper) || throw(ArgumentError("LCU must be one of :lower, :center, :upper")) bend >= bstart || throw(ArgumentError("bend should be greater than or equal to bstart")) - return new{LCU,TF}(bstart, bend) - end - function ApproximateOctaveBands{LCU}(TF, bstart::Int, bend::Int) where {LCU} - return ApproximateOctaveBands{LCU,TF}(bstart, bend) + scaler > 0 || throw(ArgumentError("non-positive scaler argument not supported")) + return new{LCU,TF}(bstart, bend, TF(scaler)) end end """ ApproximateOctaveBands{LCU,TF}(bstart::Int, bend::Int) -Construct an `ApproximateOctaveBands` with `eltype` `TF` encomposing band numbers from `bstart` to `bend`. +Construct an `ApproximateOctaveBands` with `eltype` `TF` encomposing band index numbers from `bstart` to `bend`. -`TF` defaults to `Float64`. +The "standard" band frequencies will be scaled by `scaler`, e.g. if `scaler = 0.5` then what would normally be the `1000 Hz` frequency will be `500 Hz`, etc.. """ -ApproximateOctaveBands{LCU}(bstart::Int, bend::Int) where {LCU} = ApproximateOctaveBands{LCU,Float64}(bstart, bend) - -""" - Base.getindex(bands::ApproximateOctaveBands{LCU}, i::Int) where {LCU} - -Return the lower, center, or upper frequency (depending on the value of `LCU`) associated with the `i`-th proportional band frequency covered by `bands`. -""" -Base.getindex(bands::ApproximateOctaveBands, i::Int) +function ApproximateOctaveBands{LCU}(TF::Type, bstart::Int, bend::Int, scaler=1) where {LCU} + return ApproximateOctaveBands{LCU,TF}(bstart, bend, scaler) +end +function ApproximateOctaveBands{LCU}(bstart::Int, bend::Int, scaler=1) where {LCU} + return ApproximateOctaveBands{LCU}(Float64, bstart, bend, scaler) +end @inline function Base.getindex(bands::ApproximateOctaveBands{:center,TF}, i::Int) where {TF} @boundscheck checkbounds(bands, i) j = bands.bstart + i - 1 factor1000, b0 = divrem(j, 10, RoundDown) b = b0 + 1 - return approx_octave_cbands_pattern[b]*TF(1000)^factor1000 + return freq_scaler(bands)*approx_octave_cbands_pattern[b]*TF(1000)^factor1000 end @inline function Base.getindex(bands::ApproximateOctaveBands{:lower,TF}, i::Int) where {TF} @@ -323,7 +492,7 @@ end j = bands.bstart + i - 1 factor1000, b0 = divrem(j, 10, RoundDown) b = b0 + 1 - return approx_octave_lbands_pattern[b]*TF(1000)^factor1000 + return freq_scaler(bands)*approx_octave_lbands_pattern[b]*TF(1000)^factor1000 end @inline function Base.getindex(bands::ApproximateOctaveBands{:upper,TF}, i::Int) where {TF} @@ -331,12 +500,12 @@ end j = bands.bstart + i - 1 factor1000, b0 = divrem(j, 10, RoundDown) b = b0 + 1 - return approx_octave_ubands_pattern[b]*TF(1000)^factor1000 + return freq_scaler(bands)*approx_octave_ubands_pattern[b]*TF(1000)^factor1000 end -@inline function band_approx_octave_lower_limit(fl::TF) where {TF} - factor1000 = floor(Int, log10(fl/approx_octave_lbands_pattern[1])/3) - i = searchsortedfirst(approx_octave_lbands_pattern, fl; lt=(lband, f)->isless(lband*TF(10)^(3*factor1000), f)) +@inline function band_approx_octave_lower_limit(fl::TF, scaler) where {TF} + factor1000 = floor(Int, log10(fl/(scaler*approx_octave_lbands_pattern[1]))/3) + i = searchsortedfirst(approx_octave_lbands_pattern, fl; lt=(lband, f)->isless(scaler*lband*TF(10)^(3*factor1000), f)) # - 2 because # # * -1 for searchsortedfirst giving us the first index in approx_octave_lbands_pattern that is greater than fl, and we want the band before that @@ -344,115 +513,355 @@ end return (i - 2) + factor1000*10 end -@inline function band_approx_octave_upper_limit(fu::TF) where {TF} - factor1000 = floor(Int, log10(fu/approx_octave_lbands_pattern[1])/3) - i = searchsortedfirst(approx_octave_ubands_pattern, fu; lt=(lband, f)->isless(lband*TF(10)^(3*factor1000), f)) +@inline function band_approx_octave_upper_limit(fu::TF, scaler) where {TF} + factor1000 = floor(Int, log10(fu/(scaler*approx_octave_lbands_pattern[1]))/3) + i = searchsortedfirst(approx_octave_ubands_pattern, fu; lt=(lband, f)->isless(scaler*lband*TF(10)^(3*factor1000), f)) # - 1 because # # * -1 because the array approx_octave_lbands_pattern is 1-based, but the octave band pattern band numbers are 0-based (centerband 1.0 Hz is band number 0, etc..) return (i - 1) + factor1000*10 end +function cband_approx_octave(fc, scaler) + fc_scaled = fc/scaler + frac, factor1000 = modf(log10(fc_scaled)/log10(1000)) + # if (frac < -eps(frac)) + # frac += 1 + # factor1000 -= 1 + # end + adj = ifelse(frac < -eps(frac), 1, 0) + frac += adj + factor1000 -= adj + cband_pattern_entry = 1000^frac + tol_shift = 0.001 + b = searchsortedfirst(approx_octave_cbands_pattern, cband_pattern_entry-tol_shift) + tol_compare = 100*eps(approx_octave_cbands_pattern[b]) + abs_cs_safe(approx_octave_cbands_pattern[b] - cband_pattern_entry) < tol_compare || throw(ArgumentError("frequency f does not correspond to an approximate octave center band")) + b0 = b - 1 + j = 10*Int(factor1000) + b0 + return j +end + +function cband_number(::Type{<:ApproximateOctaveBands}, fc, scaler) + return cband_approx_octave(fc, scaler) +end + +""" + ApproximateOctaveBands{LCU}(fstart::TF, fend::TF, scaler=1) + +Construct an `ApproximateOctaveBands` with `eltype` `TF`, scaled by `scaler`, encomposing the bands needed to completely extend over minimum frequency `fstart` and maximum frequency `fend`. """ - ApproximateOctaveBands{LCU}(fstart::TF, fend::TF) +ApproximateOctaveBands{LCU}(fstart::TF, fend::TF, scaler=1) where {LCU,TF} = ApproximateOctaveBands{LCU,TF}(fstart, fend, scaler) +ApproximateOctaveBands{LCU,TF}(fstart::TF, fend::TF, scaler=1) where {LCU,TF} = ApproximateOctaveBands{LCU,TF}(band_approx_octave_lower_limit(fstart, scaler), band_approx_octave_upper_limit(fend, scaler), scaler) -Construct an `ApproximateOctaveBands` with `eltype` `TF` encomposing the bands needed to completly extend over minimum frequency `fstart` and maximum frequency `fend`. """ -ApproximateOctaveBands{LCU}(fstart::TF, fend::TF) where {LCU,TF} = ApproximateOctaveBands{LCU,TF}(fstart, fend) -ApproximateOctaveBands{LCU,TF}(fstart::TF, fend::TF) where {LCU,TF} = ApproximateOctaveBands{LCU,TF}(band_approx_octave_lower_limit(fstart), band_approx_octave_upper_limit(fend)) + ApproximateOctaveCenterBands{TF} +Alias for `ApproximateOctaveBands{:center,TF}` +""" const ApproximateOctaveCenterBands{TF} = ApproximateOctaveBands{:center,TF} + +""" + ApproximateOctaveLowerBands{TF} + +Alias for `ApproximateOctaveBands{:lower,TF}` +""" const ApproximateOctaveLowerBands{TF} = ApproximateOctaveBands{:lower,TF} + +""" + ApproximateOctaveUpperBands{TF} + +Alias for `ApproximateOctaveBands{:upper,TF}` +""" const ApproximateOctaveUpperBands{TF} = ApproximateOctaveBands{:upper,TF} +""" + lower_bands(bands::ApproximateOctaveBands{LCU,TF}, scaler=freq_scaler(bands)) + +Construct and return the lower edges of the proportional bands `bands` scaled by `scaler`. +""" +lower_bands(bands::ApproximateOctaveBands{LCU,TF}, scaler=freq_scaler(bands)) where {LCU,TF} = ApproximateOctaveBands{:lower,TF}(band_start(bands), band_end(bands), scaler) """ - ProportionalBandSpectrum{NO,TF,TAmp,TBandsL,TBandsC,TBandsU} + center_bands(bands::ApproximateOctaveBands{LCU,TF}, scaler=freq_scaler(bands)) -Representation of a proportional band spectrum with octave fraction `NO` and `eltype` `TF`. +Construct and return the centers of the proportional bands `bands` scaled by `scaler`. +""" +center_bands(bands::ApproximateOctaveBands{LCU,TF}, scaler=freq_scaler(bands)) where {LCU,TF} = ApproximateOctaveBands{:center,TF}(band_start(bands), band_end(bands), scaler) + +""" + upper_bands(bands::ApproximateOctaveBands{LCU,TF}, scaler=freq_scaler(bands)) + +Construct and return the upper edges of the proportional bands `bands` scaled by `scaler`. +""" +upper_bands(bands::ApproximateOctaveBands{LCU,TF}, scaler=freq_scaler(bands)) where {LCU,TF} = ApproximateOctaveBands{:upper,TF}(band_start(bands), band_end(bands), scaler) + +""" + AbstractProportionalBandSpectrum{NO,TF} <: AbstractVector{TF} + +Abstract type representing a proportional band spectrum with band fraction `NO` and `eltype` `TF`. +""" +abstract type AbstractProportionalBandSpectrum{NO,TF} <: AbstractVector{TF} end + +""" + octave_fraction(pbs::AbstractProportionalBandSpectrum{NO}) where {NO} + +Return `NO`, the "octave fraction," e.g. `1` for octave bands, `3` for third-octave, `12` for twelfth-octave. +""" +octave_fraction(::AbstractProportionalBandSpectrum{NO}) where {NO} = NO +octave_fraction(::Type{<:AbstractProportionalBandSpectrum{NO}}) where {NO} = NO + +""" + lower_bands(pbs::AbstractProportionalBandSpectrum) + +Return the lower edges of the proportional bands associated with the proportional band spectrum `pbs`. +""" +@inline lower_bands(pbs::AbstractProportionalBandSpectrum) = lower_bands(pbs.cbands) + +""" + center_bands(pbs::AbstractProportionalBandSpectrum) + +Return the centers of the proportional bands associated with the proportional band spectrum `pbs`. +""" +@inline center_bands(pbs::AbstractProportionalBandSpectrum) = pbs.cbands + +""" + upper_bands(pbs::AbstractProportionalBandSpectrum) + +Return the upper edges of the proportional bands associated with the proportional band spectrum `pbs`. +""" +@inline upper_bands(pbs::AbstractProportionalBandSpectrum) = upper_bands(pbs.cbands) + + +""" + freq_scaler(pbs::AbstractProportionalBandSpectrum) + +Return the factor each "standard" frequency band associated with the proportional band spectrum `pbs` is scaled by. + +For example, the approximate octave center bands include 1000 Hz, 2000 Hz, and 4000 Hz. +If `freq_scaler(pbs) == 1.0`, then these frequencies would be unchanged. +If `freq_scaler(pbs) == 1.5`, then `bands` would include 1500 Hz, 3000 Hz, and 6000 Hz instead. +If `freq_scaler(pbs) == 0.5`, then `bands` would include 500 Hz, 1000 Hz, and 2000 Hz in place of 1000 Hz, 2000 Hz, and 4000 Hz. +""" +@inline freq_scaler(pbs::AbstractProportionalBandSpectrum) = freq_scaler(center_bands(pbs)) + +""" + has_observer_time(pbs::AbstractProportionalBandSpectrum) + +Return `true` if the proportional band spectrum is defined to exist over a limited time, `false` otherwise. +""" +@inline has_observer_time(pbs::AbstractProportionalBandSpectrum) = false + +""" + observer_time(pbs::AbstractProportionalBandSpectrum) + +Return the observer time at which the proportional band spectrum is defined to exist. +""" +@inline observer_time(pbs::AbstractProportionalBandSpectrum{NO,TF}) where {NO,TF} = zero(TF) + +""" + timestep(pbs::AbstractProportionalBandSpectrum) + +Return the time range over which the proportional band spectrum is defined to exist. """ -struct ProportionalBandSpectrum{NO,TF,TAmp,TBandsL<:AbstractProportionalBands{NO,:lower,TF},TBandsC<:AbstractProportionalBands{NO,:center,TF},TBandsU<:AbstractProportionalBands{NO,:upper,TF}} <: AbstractVector{TF} +@inline timestep(pbs::AbstractProportionalBandSpectrum) = Inf*one(eltype(pbs)) + +""" + amplitude(pbs::AbstractProportionalBandSpectrum) + +Return the underlying `Vector` containing the proportional band spectrum amplitudes contained in `pbs`. +""" +@inline amplitude(pbs::AbstractProportionalBandSpectrum) = pbs.pbs + +""" + time_period(pbs::AbstractArray{<:AbstractProportionalBandSpectrum}) + +Find the period of time over which the collection of proportional band spectrum `pbs` exists. +""" +function time_period(pbs::AbstractArray{<:AbstractProportionalBandSpectrum}) + tmin, tmax = extrema(observer_time, Iterators.filter(has_observer_time, pbs); init=(Inf, -Inf)) + return tmax - tmin +end + +""" + time_scaler(pbs::AbstractProportionalBandSpectrum{NO,TF}, period) + +Find the scaling factor appropriate to multiply the proportional band spectrum `pbs` by that accounts for the duration of time the spectrum exists. + +This is used when combining multiple proportional band spectra with the [combine](@ref) function. +""" +time_scaler(pbs::AbstractProportionalBandSpectrum{NO,TF}, period) where {NO,TF} = one(TF) + +@inline Base.size(pbs::AbstractProportionalBandSpectrum) = size(center_bands(pbs)) + +@inline function Base.getindex(pbs::AbstractProportionalBandSpectrum, i::Int) + @boundscheck checkbounds(pbs, i) + return @inbounds amplitude(pbs)[i] +end + +""" + LazyNBProportionalBandSpectrum{NO,IsTonal,TF,TAmp,TBandsC} + +Lazy representation of a proportional band spectrum with octave fraction `NO` and `eltype` `TF` constructed from a narrowband (`NB`) spectrum. + +`IsTonal` indicates how the acoustic energy is distributed through the narrow frequency bands: + + * `IsTonal == false` means the acoustic energy is assumed to be evenly distributed thoughout each band + * `IsTonal == true` means the acoustic energy is assumed to be concentrated at each band center +""" +struct LazyNBProportionalBandSpectrum{NO,IsTonal,TF,TAmp<:AbstractVector{TF},TBandsC<:AbstractProportionalBands{NO,:center}} <: AbstractProportionalBandSpectrum{NO,TF} f1_nb::TF df_nb::TF - psd_amp::TAmp - lbands::TBandsL + msp_amp::TAmp cbands::TBandsC - ubands::TBandsU - - function ProportionalBandSpectrum(TBands::Type{<:AbstractProportionalBands{NO}}, f1_nb, df_nb, psd_amp) where {NO} - TF = promote_type(typeof(f1_nb), typeof(df_nb), eltype(psd_amp)) + function LazyNBProportionalBandSpectrum{NO,IsTonal,TF,TAmp}(f1_nb::TF, df_nb::TF, msp_amp::TAmp, cbands::AbstractProportionalBands{NO,:center}) where {NO,IsTonal,TF,TAmp<:AbstractVector{TF}} f1_nb > zero(f1_nb) || throw(ArgumentError("f1_nb must be > 0")) - # We're thinking of each non-zero freqeuncy as being a bin with center frequency `f` and width `df_nb`. - # So to get the lowest non-zero frequency we'll subtract 0.5*df_nb from the lowest non-zero frequency center: - fstart = max(f1_nb - 0.5*df_nb, TF(fmin_exact)) - fend = f1_nb + (length(psd_amp)-1)*df_nb + 0.5*df_nb + df_nb > zero(df_nb) || throw(ArgumentError("df_nb must be > 0")) + return new{NO,IsTonal,TF,TAmp,typeof(cbands)}(f1_nb, df_nb, msp_amp, cbands) + end +end - lbands = TBands{:lower}(fstart, fend) - cbands = TBands{:center}(TF, band_start(lbands), band_end(lbands)) - ubands = TBands{:upper}(TF, band_start(lbands), band_end(lbands)) +""" + LazyNBProportionalBandSpectrum{NO,IsTonal}(f1_nb, df_nb, msp_amp, cbands::AbstractProportionalBands{NO,:center}) - return new{NO,TF,typeof(psd_amp),typeof(lbands), typeof(cbands), typeof(ubands)}(f1_nb, df_nb, psd_amp, lbands, cbands, ubands) - end +Construct a lazy representation of a proportional band spectrum with proportional center bands `cbands` from a narrowband spectrum. + +The narrowband frequencies are defined by the first narrowband frequency `f1_nb` and the narrowband frequency spacing `df_nb`. +`msp_amp` is the spectrum of narrowband mean squared pressure amplitude. + +`IsTonal` indicates how the acoustic energy is distributed through the narrow frequency bands: + + * `IsTonal == false` means the acoustic energy is assumed to be evenly distributed thoughout each band + * `IsTonal == true` means the acoustic energy is assumed to be concentrated at each band center +""" +function LazyNBProportionalBandSpectrum{NO,IsTonal}(f1_nb, df_nb, msp_amp, cbands::AbstractProportionalBands{NO,:center}) where {NO,IsTonal} + TF = eltype(msp_amp) + TAmp = typeof(msp_amp) + return LazyNBProportionalBandSpectrum{NO,IsTonal,TF,TAmp}(TF(f1_nb), TF(df_nb), msp_amp, cbands) +end + +""" + LazyNBProportionalBandSpectrum(f1_nb, df_nb, msp_amp, cbands::AbstractProportionalBands{NO,:center}, istonal=false) + +Construct a lazy representation of a proportional band spectrum with proportional center bands `cbands` from a narrowband spectrum. + +The narrowband frequencies are defined by the first narrowband frequency `f1_nb` and the narrowband frequency spacing `df_nb`. +`msp_amp` is the spectrum of narrowband mean squared pressure amplitude. + +`istonal` indicates how the acoustic energy is distributed through the narrow frequency bands: + + * `istonal == false` means the acoustic energy is assumed to be evenly distributed thoughout each band + * `istonal == true` means the acoustic energy is assumed to be concentrated at each band center +""" +function LazyNBProportionalBandSpectrum(f1_nb, df_nb, msp_amp, cbands::AbstractProportionalBands{NO,:center}, istonal::Bool=false) where {NO} + return LazyNBProportionalBandSpectrum{NO,istonal}(f1_nb, df_nb, msp_amp, cbands) end -const ExactOctaveSpectrum{TF,TAmp} = ProportionalBandSpectrum{1,TF,TAmp, - ExactProportionalBands{1,:lower,TF}, - ExactProportionalBands{1,:center,TF}, - ExactProportionalBands{1,:upper,TF}} -ExactOctaveSpectrum(f1_nb, df_nb, psd_amp) = ProportionalBandSpectrum(ExactProportionalBands{1}, f1_nb, df_nb, psd_amp) -ExactOctaveSpectrum(sm::AbstractNarrowbandSpectrum) = ProportionalBandSpectrum(ExactProportionalBands{1}, sm) +""" + LazyNBProportionalBandSpectrum{NO,IsTonal}(TBands::Type{<:AbstractProportionalBands{NO}}, f1_nb, df_nb, msp_amp, scaler=1) + +Construct a lazy representation of a proportional band spectrum with proportional band type `TBands` from a narrowband spectrum. + +The narrowband frequencies are defined by the first narrowband frequency `f1_nb` and the narrowband frequency spacing `df_nb`. +`msp_amp` is the spectrum of narrowband mean squared pressure amplitude. +The proportional band frequencies will be scaled by `scaler`. + +`IsTonal` is a `Bool` indicating how the acoustic energy is distributed through the narrow frequency bands: -const ExactThirdOctaveSpectrum{TF,TAmp} = ProportionalBandSpectrum{3,TF,TAmp, - ExactProportionalBands{3,:lower,TF}, - ExactProportionalBands{3,:center,TF}, - ExactProportionalBands{3,:upper,TF}} -ExactThirdOctaveSpectrum(f1_nb, df_nb, psd_amp) = ProportionalBandSpectrum(ExactProportionalBands{3}, f1_nb, df_nb, psd_amp) -ExactThirdOctaveSpectrum(sm::AbstractNarrowbandSpectrum) = ProportionalBandSpectrum(ExactProportionalBands{3}, sm) + * `IsTonal == false` means the acoustic energy is assumed to be evenly distributed thoughout each band + * `IsTonal == true` means the acoustic energy is assumed to be concentrated at each band center +""" +function LazyNBProportionalBandSpectrum{NO,false}(TBands::Type{<:AbstractProportionalBands{NO}}, f1_nb, df_nb, msp_amp, scaler=1) where {NO} + TF = eltype(msp_amp) + TAmp = typeof(msp_amp) + # We're thinking of each non-zero freqeuncy as being a bin with center frequency `f` and width `df_nb`. + # So to get the lowest non-zero frequency we'll subtract 0.5*df_nb from the lowest non-zero frequency center: + fstart = max(f1_nb - 0.5*df_nb, TF(fmin_exact)) + fend = f1_nb + (length(msp_amp)-1)*df_nb + 0.5*df_nb + cbands = TBands{:center}(fstart, fend, scaler) + + return LazyNBProportionalBandSpectrum{NO,false,TF,TAmp}(TF(f1_nb), TF(df_nb), msp_amp, cbands) +end +function LazyNBProportionalBandSpectrum{NO,true}(TBands::Type{<:AbstractProportionalBands{NO}}, f1_nb, df_nb, msp_amp, scaler=1) where {NO} + TF = eltype(msp_amp) + TAmp = typeof(msp_amp) + # We're thinking of each non-zero freqeuncy as being an infinitely thin "bin" with center frequency `f` and spacing `df_nb`. + # So to get the lowest non-zero frequency is f1_nb, and the highest is f1_nb + (length(msp_amp)-1)*df_nb. + fstart = f1_nb + fend = f1_nb + (length(msp_amp)-1)*df_nb + cbands = TBands{:center}(fstart, fend, scaler) + + return LazyNBProportionalBandSpectrum{NO,true,TF,TAmp}(TF(f1_nb), TF(df_nb), msp_amp, cbands) +end -const ApproximateOctaveSpectrum{TF,TAmp} = ProportionalBandSpectrum{1,TF,TAmp, - ApproximateOctaveBands{:lower,TF}, - ApproximateOctaveBands{:center,TF}, - ApproximateOctaveBands{:upper,TF}} -ApproximateOctaveSpectrum(f1_nb, df_nb, psd_amp) = ProportionalBandSpectrum(ApproximateOctaveBands, f1_nb, df_nb, psd_amp) -ApproximateOctaveSpectrum(sm::AbstractNarrowbandSpectrum) = ProportionalBandSpectrum(ApproximateOctaveBands, sm) +""" + LazyNBProportionalBandSpectrum(TBands::Type{<:AbstractProportionalBands}, f1_nb, df_nb, msp_amp, scaler=1, istonal::Bool=false) -const ApproximateThirdOctaveSpectrum{TF,TAmp} = ProportionalBandSpectrum{1,TF,TAmp, - ApproximateThirdOctaveBands{:lower,TF}, - ApproximateThirdOctaveBands{:center,TF}, - ApproximateThirdOctaveBands{:upper,TF}} -ApproximateThirdOctaveSpectrum(f1_nb, df_nb, psd_amp) = ProportionalBandSpectrum(ApproximateThirdOctaveBands, f1_nb, df_nb, psd_amp) -ApproximateThirdOctaveSpectrum(sm::AbstractNarrowbandSpectrum) = ProportionalBandSpectrum(ApproximateThirdOctaveBands, sm) +Construct a `LazyNBProportionalBandSpectrum` using proportional bands `TBands` and narrowband mean squared pressure amplitude vector `msp_amp` and optional proportional band frequency scaler `scaler`. -frequency_nb(pbs::ProportionalBandSpectrum) = pbs.f1_nb .+ (0:length(pbs.psd_amp)-1).*pbs.df_nb +`f1_nb` is the first non-zero narrowband frequency, and `df_nb` is the narrowband frequency spacing. +The `istonal` `Bool` argument, if `true`, indicates the narrowband spectrum is tonal and thus concentrated at discrete frequencies. +If `false`, the spectrum is assumed to be constant over each narrow frequency band. +The proportional band frequencies will be scaled by `scaler`. +""" +function LazyNBProportionalBandSpectrum(TBands::Type{<:AbstractProportionalBands{NO}}, f1_nb, df_nb, msp_amp, scaler=1, istonal::Bool=false) where {NO} + return LazyNBProportionalBandSpectrum{NO,istonal}(TBands, f1_nb, df_nb, msp_amp, scaler) +end """ - ProportionalBandSpectrum(TBands::Type{<:AbstractProportionalBands}, sm::AbstractNarrowbandSpectrum) + LazyNBProportionalBandSpectrum(TBands::Type{<:AbstractProportionalBands}, sm::AbstractNarrowbandSpectrum, scaler=1) -Construct a `ProportionalBandSpectrum` using a proportional band `TBands` and narrowband spectrum `sm`. +Construct a `LazyNBProportionalBandSpectrum` using a proportional band `TBands` and narrowband spectrum `sm`, and optional frequency scaler `scaler`. +The proportional band frequencies will be scaled by `scaler`. """ -function ProportionalBandSpectrum(TBands::Type{<:AbstractProportionalBands}, sm::AbstractNarrowbandSpectrum) - psd = PowerSpectralDensityAmplitude(sm) - freq = frequency(psd) +function LazyNBProportionalBandSpectrum(TBands::Type{<:AbstractProportionalBands{NO}}, sm::AbstractNarrowbandSpectrum{IsEven,IsTonal}, scaler=1) where {NO,IsEven,IsTonal} + msp = MSPSpectrumAmplitude(sm) + freq = frequency(msp) f1_nb = freq[begin+1] df_nb = step(freq) # Skip the zero frequency. - psd_amp = @view psd[begin+1:end] - return ProportionalBandSpectrum(TBands, f1_nb, df_nb, psd_amp) + msp_amp = @view msp[begin+1:end] + return LazyNBProportionalBandSpectrum{NO,IsTonal}(TBands, f1_nb, df_nb, msp_amp, scaler) end -@inline lower_bands(pbs::ProportionalBandSpectrum) = pbs.lbands -@inline center_bands(pbs::ProportionalBandSpectrum) = pbs.cbands -@inline upper_bands(pbs::ProportionalBandSpectrum) = pbs.ubands +""" + LazyNBProportionalBandSpectrum(sm::AbstractNarrowbandSpectrum, cbands::AbstractProportionalBands{NO,:center}) + +Construct a `LazyNBProportionalBandSpectrum` using proportional centerbands `cbands` and narrowband spectrum `sm`. +The proportional band frequencies will be scaled by `scaler`. +""" +function LazyNBProportionalBandSpectrum(sm::AbstractNarrowbandSpectrum{IsEven,IsTonal}, cbands::AbstractProportionalBands{NO,:center}) where {NO,IsEven,IsTonal} + msp = MSPSpectrumAmplitude(sm) + TF = eltype(msp) + freq = frequency(msp) + f1_nb = TF(freq[begin+1]) + df_nb = TF(step(freq)) + # Skip the zero frequency. + msp_amp = @view msp[begin+1:end] + TAmp = typeof(msp_amp) + return LazyNBProportionalBandSpectrum{NO,IsTonal,TF,TAmp}(f1_nb, df_nb, msp_amp, cbands) +end -@inline Base.size(pbs::ProportionalBandSpectrum) = size(center_bands(pbs)) +""" + frequency_nb(pbs::LazyNBProportionalBandSpectrum) +Return the narrowband frequencies associated with the underlying narrowband spectrum contained in `pbs`. """ - Base.getindex(pbs::ProportionalBandSpectrum, i::Int) +frequency_nb(pbs::LazyNBProportionalBandSpectrum) = pbs.f1_nb .+ (0:length(pbs.msp_amp)-1).*pbs.df_nb -Return the proportional band spectrum amplitude for the `i`th non-zero band in `pbs`. """ -@inline function Base.getindex(pbs::ProportionalBandSpectrum, i::Int) + lazy_pbs(pbs, cbands::AbstractProportionalBands{NO,:center}) + +Construct a lazy proportional band spectrum on proportional center bands `cbands` using the proportional band spectrum `pbs`. +""" +lazy_pbs + +function lazy_pbs(pbs::LazyNBProportionalBandSpectrum{NOIn,IsTonal}, cbands::AbstractProportionalBands{NO,:center}) where {NOIn,IsTonal,NO} + return LazyNBProportionalBandSpectrum{NO,IsTonal}(pbs.f1_nb, pbs.df_nb, pbs.msp_amp, cbands) +end + +@inline function Base.getindex(pbs::LazyNBProportionalBandSpectrum{NO,false}, i::Int) where {NO} @boundscheck checkbounds(pbs, i) # This is where the fun begins. # So, first I want the lower and upper bands of this band. @@ -485,14 +894,14 @@ Return the proportional band spectrum amplitude for the `i`th non-zero band in ` iend = searchsortedlast(f_nb, fu + 0.5*Δf) if iend == 0 # All the frequencies are lower than the band we're looking for. - return zero(eltype(pds)) + return zero(eltype(pbs)) end - # Need the psd amplitude relavent for this band. - # First, get all of the psd amplitudes. - psd_amp = pbs.psd_amp + # Need the msp amplitude relavent for this band. + # First, get all of the msp amplitudes. + msp_amp = pbs.msp_amp # Now get the amplitudes we actually want. - psd_amp_v = @view psd_amp[istart:iend] + msp_amp_v = @view msp_amp[istart:iend] f_nb_v = @view f_nb[istart:iend] # Get the contribution of the first band, which might not be a full band. @@ -502,20 +911,334 @@ Return the proportional band spectrum amplitude for the `i`th non-zero band in ` # proportional bands. If that's the case, then we need to clip it to the proportional band width. band_lhs = max(f_nb_v[1] - 0.5*Δf, fl) band_rhs = min(f_nb_v[1] + 0.5*Δf, fu) - res_first_band = psd_amp_v[1]*(band_rhs - band_lhs) - # @show i res_first_band + res_first_band = msp_amp_v[1]/Δf*(band_rhs - band_lhs) # Get the contribution of the last band, which might not be a full band. - if length(psd_amp_v) > 1 + if length(msp_amp_v) > 1 band_lhs = max(f_nb_v[end] - 0.5*Δf, fl) band_rhs = min(f_nb_v[end] + 0.5*Δf, fu) - res_last_band = psd_amp_v[end]*(band_rhs - band_lhs) + res_last_band = msp_amp_v[end]/Δf*(band_rhs - band_lhs) else res_last_band = zero(eltype(pbs)) end # Get all the others and return them. - psd_amp_v2 = @view psd_amp_v[2:end-1] - res = res_first_band + res_last_band - return res_first_band + sum(psd_amp_v2*Δf) + res_last_band + msp_amp_v2 = @view msp_amp_v[2:end-1] + return res_first_band + sum(msp_amp_v2) + res_last_band +end + +@inline function Base.getindex(pbs::LazyNBProportionalBandSpectrum{NO,true}, i::Int) where {NO} + @boundscheck checkbounds(pbs, i) + # This is where the fun begins. + # So, first I want the lower and upper bands of this band. + fl = lower_bands(pbs)[i] + # Arg, numerical problems: lower_bands[i+1] should be the same as upper_bands[i]. + # But because of floating point inaccuracies, they can be a tiny bit different. + # And that can lead to a "gap" between, say, upper_bands[i] and lower_bands[i+1]. + # And then if a tone is right in that gap, we'll miss part of the spectrum. + # So, to fix this, always use the lower band values except for the last proportional band (where it won't matter, since that frequency value is only used once, and hence there can't be any gap). + if i < length(pbs) + fu = lower_bands(pbs)[i+1] + else + fu = upper_bands(pbs)[i] + end + # Now I need to find the starting and ending indices that are included in this frequency band. + + # Need the narrowband frequencies. + # This will not include the zero frequency. + f_nb = frequency_nb(pbs) + + # This is the narrowband frequency spacing. + Δf = pbs.df_nb + + # So, what is the first index we want? + # It's the one that has f_nb[i] >= fl. + istart = searchsortedfirst(f_nb, fl) + # `searchsortedfirst` will return `length(f_nb)+1` it doesn't find anything. + # What does that mean? + # That means that all the frequencies in the narrowband spectrum are lower + # than the band we're looking at. So return 0. + if istart == length(f_nb) + 1 + return zero(eltype(pbs)) + end + + # What is the last index we want? + # It's the last one that has f_nb[i] <= fu + # iend = searchsortedlast(f_nb, fu) + # But we don't want to double-count frequencies, so we actually want f_nb[i] < fu. + # Could just do `searchsortedlast(f_nb, fu; lt=<=)`, but this avoids the possibly-slow keyword arguments. + iend = searchsortedlast(f_nb, fu, ord(<=, identity, nothing, Forward)) + if iend == 0 + # All the frequencies are lower than the band we're looking for. + return zero(eltype(pbs)) + end + + # Need the msp amplitude relavent for this band. + # First, get all of the msp amplitudes. + msp_amp = pbs.msp_amp + # Now get the amplitudes we actually want. + msp_amp_v = @view msp_amp[istart:iend] + + # Since we're thinking of the narrowband frequency bins as being infinitely thin, they can't partially extend beyond the lower or upper limits of the relevant proportional band. + # So we just need to add them up here: + return sum(msp_amp_v) +end + +""" + ProportionalBandSpectrum{NO,TF,TPBS,TBandsL,TBandsC,TBandsU} + +Representation of a proportional band spectrum with octave fraction `NO` and `eltype` `TF`. +""" +struct ProportionalBandSpectrum{NO,TF,TPBS<:AbstractVector{TF},TBandsC<:AbstractProportionalBands{NO,:center}} <: AbstractProportionalBandSpectrum{NO,TF} + pbs::TPBS + cbands::TBandsC + + function ProportionalBandSpectrum(pbs, cbands::AbstractProportionalBands{NO,:center}) where {NO} + length(pbs) == length(cbands) || throw(ArgumentError("length(pbs) must match length(cbands)")) + return new{NO,eltype(pbs),typeof(pbs),typeof(cbands)}(pbs, cbands) + end +end + +function lazy_pbs(pbs::ProportionalBandSpectrum, cbands::AbstractProportionalBands{NO,:center}) where {NO} + return LazyPBSProportionalBandSpectrum(pbs, cbands) +end + +""" + ProportionalBandSpectrum(TBandsC, cfreq_start, pbs, scaler=1) + +Construct a `ProportionalBandSpectrum` from an array of proportional band amplitudes and proportional band type `TBandsC`. + +`cfreq_start` is the centerband frequency corresponding to the first entry of `pbs`. +The proportional band frequencies indicated by `TBandsC` are multiplied by `scaler`. +""" +function ProportionalBandSpectrum(TBandsC::Type{<:AbstractProportionalBands{NO,:center}}, cfreq_start, pbs, scaler=1) where {NO} + bstart = cband_number(TBandsC, cfreq_start, scaler) + bend = bstart + length(pbs) - 1 + cbands = TBandsC(bstart, bend, scaler) + + return ProportionalBandSpectrum(pbs, cbands) +end + +""" + ProportionalBandSpectrumWithTime{NO,TF,TPBS,TBandsC,TTime,TDTime} + +Representation of a proportional band spectrum with octave fraction `NO` and `eltype` `TF`, but with an observer time. +""" +struct ProportionalBandSpectrumWithTime{NO,TF,TPBS<:AbstractVector{TF},TBandsC<:AbstractProportionalBands{NO,:center},TDTime,TTime} <: AbstractProportionalBandSpectrum{NO,TF} + pbs::TPBS + cbands::TBandsC + dt::TDTime + t::TTime + + @doc """ + ProportionalBandSpectrumWithTime(pbs, cbands::AbstractProportionalBands{NO,:center}, dt, t) + + Construct a proportional band spectrum from mean-squared pressure amplitudes `pbs` and centerband frequencies `cbands`, defined to exist over time range `dt` and at observer time `t`. + """ + function ProportionalBandSpectrumWithTime(pbs, cbands::AbstractProportionalBands{NO,:center}, dt, t) where {NO} + length(pbs) == length(cbands) || throw(ArgumentError("length(pbs) must match length(cbands)")) + dt > zero(dt) || throw(ArgumentError("dt must be positive")) + + return new{NO,eltype(pbs),typeof(pbs),typeof(cbands),typeof(dt),typeof(t)}(pbs, cbands, dt, t) + end +end + +@inline has_observer_time(pbs::ProportionalBandSpectrumWithTime) = true +@inline observer_time(pbs::ProportionalBandSpectrumWithTime) = pbs.t +@inline timestep(pbs::ProportionalBandSpectrumWithTime{NO,TF}) where {NO,TF} = pbs.dt +@inline time_scaler(pbs::ProportionalBandSpectrumWithTime, period) = timestep(pbs)/period + +function lazy_pbs(pbs::ProportionalBandSpectrumWithTime, cbands::AbstractProportionalBands{NO,:center}) where {NO} + return LazyPBSProportionalBandSpectrum(pbs, cbands) +end + +""" + LazyPBSProportionalBandSpectrum{NO,TF} <: AbstractProportionalBandSpectrum{NO,TF} + +Lazy representation of a proportional band spectrum with octave fraction `NO` and `eltype` `TF` constructed from a different proportional band spectrum. +""" +struct LazyPBSProportionalBandSpectrum{NO,TF,TPBS<:AbstractProportionalBandSpectrum,TBandsC<:AbstractProportionalBands{NO,:center}} <: AbstractProportionalBandSpectrum{NO,TF} + pbs::TPBS + cbands::TBandsC + + function LazyPBSProportionalBandSpectrum(pbs::AbstractProportionalBandSpectrum{NOIn,TF}, cbands::AbstractProportionalBands{NO,:center}) where {NO,TF,NOIn} + return new{NO,TF,typeof(pbs),typeof(cbands)}(pbs, cbands) + end +end + +function LazyPBSProportionalBandSpectrum(TBands::Type{<:AbstractProportionalBands{NO}}, pbs::AbstractProportionalBandSpectrum, scaler=1) where {NO} + # First, get the minimum and maximum frequencies associated with the input pbs. + fstart = lower_bands(pbs)[begin] + fend = upper_bands(pbs)[end] + # Now use those frequencies to construct some centerbands. + cbands = TBands{:center}(fstart, fend, scaler) + # Now we can create the object. + return LazyPBSProportionalBandSpectrum(pbs, cbands) +end + +@inline has_observer_time(pbs::LazyPBSProportionalBandSpectrum) = has_observer_time(pbs.pbs) +@inline observer_time(pbs::LazyPBSProportionalBandSpectrum) = observer_time(pbs.pbs) +@inline timestep(pbs::LazyPBSProportionalBandSpectrum) = timestep(pbs.pbs) +@inline time_scaler(pbs::LazyPBSProportionalBandSpectrum, period) = time_scaler(pbs.pbs, period) + +function lazy_pbs(pbs::LazyPBSProportionalBandSpectrum, cbands::AbstractProportionalBands{NO,:center}) where {NO} + return LazyPBSProportionalBandSpectrum(pbs.pbs, cbands) +end + +@inline function Base.getindex(pbs::LazyPBSProportionalBandSpectrum, i::Int) + @boundscheck checkbounds(pbs, i) + + # So, first I want the lower and upper bands of this output band. + fol = lower_bands(pbs)[i] + fou = upper_bands(pbs)[i] + + # Get the underlying pbs. + pbs_in = pbs.pbs + + # Get the lower and upper edges of the input band's spectrum. + inbands_lower = lower_bands(pbs_in) + inbands_upper = upper_bands(pbs_in) + + # So now I have the boundaries of the frequencies I'm interested in in `fol` and `fou`. + # What I'm looking for now is: + # + # * the first input band whose upper edge is greater than `fol` + # * the last input band whose lower edge is less than `fou`. + # + # So, for the first input band whose upper edge is greater than `fol`, I should be able to do this: + istart = searchsortedfirst(inbands_upper, fol) + + # For that, what if + # + # * All of `inbands_upper` are less than `fol`? + # That would mean all of the `inband` frequencies are lower than and outside the current `outband`. + # Then the docs for `searchsortedfirst` say that it will return `length(inbands_upper)+1`. + # So if I started a view of the data from that index, it would obviously be empty, which is what I'd want. + # * All of the `inbands_upper` are greater than `fol`? + # Not necessarily a problem, unless, I guess, the lowest of `inbands_lower` is *also* greater than `fou`. + # Then the entire input spectrum would be larger than this band. + # But `searchsortedfirst` should just return `1`, and hopefully that would be the right thing. + + # Now I want the last input band whose lower edge is less than `fou`. + # I should be able to get that from + iend = searchsortedlast(inbands_lower, fou) + # For that, what if + # + # * All of the `inbands_lower` are greater than `fou`? + # That would mean all of the `inband` frequencies are greater than and outside the current `outband`. + # The docs indicate `searchsortedlast` would return `firstindex(inbands_lower)-1` for that case, i.e. `0`. + # That's what I'd want, I think. + # * All of the `inbands_lower` are lower than `fou`? + # Not necessarily a problem, unless the highest of `inbands_upper` are also lower than `fou`, which would mean the entire input spectrum is lower than this output band. + + + # Now I have the first and last input bands relevant to this output band, and so I can start adding up the input PBS's contributions to this output band. + pbs_out = zero(eltype(pbs)) + + # First, we need to check that there's something to do: + if (istart <= lastindex(pbs_in)) && (iend >= firstindex(pbs_in)) + + # First, get the bandwidth of the first input band associated with this output band. + fil_start = inbands_lower[istart] + fiu_start = inbands_upper[istart] + dfin_start = fiu_start - fil_start + + # Next, need to get the frequency overlap of the first input band and this output band. + # For the lower edge of the overlap, it will usually be `fol`, unless there's a gap where `inbands_lower[istart]` is greater than `fol`. + foverlapl_start = max(fol, fil_start) + # For the upper edge of the overlap, it will usually be `fiu_start`, unless there's a gap where `inbands_upper[istart]` is less than `fou`. + foverlapu_start = min(fou, fiu_start) + + # Now get the first band's contribution to the PBS. + pbs_out += pbs_in[istart]/dfin_start*(foverlapu_start - foverlapl_start) + + # Now, think about the last band's contribution to the PBS. + # First, we need to check if the first and last band are identicial, which would indicate that there's only one input band in this output band. + if iend > istart + # Now need to get the bandwidth associated with this input band. + fil_end = inbands_lower[iend] + fiu_end = inbands_upper[iend] + dfin_end = fiu_end - fil_end + + # Next, need to get the frequency overlap of the last input band and this output band. + foverlapl_end = max(fol, fil_end) + foverlapu_end = min(fou, fiu_end) + + # Now we can get the last band's contribution to the PBS. + pbs_out += pbs_in[iend]/dfin_end*(foverlapu_end - foverlapl_end) + + # Now we need the contribution of the input bands between `istart+1` and `iend-1`, inclusive. + # Don't need to worry about incomplete overlap of the bands since these are "inside" this output band, so we can just directly sum them. + pbs_in_v = @view pbs_in[istart+1:iend-1] + pbs_out += sum(pbs_in_v) + end + end + + return pbs_out +end + +""" + combine(pbs::AbstractArray{<:AbstractProportionalBandSpectrum}, outcbands::AbstractProportionalBands{NO,:center}, time_axis=1) where {NO} + +Combine each input proportional band spectrum of `pbs` into one output proportional band spectrum using the proportional center bands indicated by `outcbands`. + +`time_axis` is an integer indicating the axis of the `pbs` array along which time varies. +For example, if `time_axis == 1` and `pbs` is a three-dimensional array, then `apth[:, i, j]` would be proportional band spectrum of source `i`, `j` for all time. +But if `time_axis == 3`, then `pbs[i, j, :]` would be the proportional band spectrum of source `i`, `j` for all time. +""" +function combine(pbs::AbstractArray{<:AbstractProportionalBandSpectrum}, outcbands::AbstractProportionalBands{NO,:center}, time_axis=1) where {NO} + # Create the vector that will contain the new PBS. + # An <:AbstractProportionalBandSpectrum is <:AbstractVector{TF}, so AbstractArray{<:AbstractProportionalBandSpectrum,N} is actually an Array of AbstractVectors. + # So `eltype(eltype(pbs))` should give me the element type of the PBS. + TFOut = promote_type(eltype(eltype(pbs)), eltype(outcbands)) + pbs_out = zeros(TFOut, length(outcbands)) + + dims_in = axes(pbs) + ndims_in = ndims(pbs) + alldims_in = 1:ndims_in + + otherdims = setdiff(alldims_in, time_axis) + itershape = tuple(dims_in[otherdims]...) + + # Create an array we'll use to index pbs_in, with a `Colon()` for the time_axis position and integers of the first value for all the others. + idx = [ifelse(d==time_axis, Colon(), first(ind)) for (d, ind) in enumerate(axes(pbs))] + + nidx = length(otherdims) + indices = CartesianIndices(itershape) + + # Loop through the indices. + for I in indices + for i in 1:nidx + idx[otherdims[i]] = I.I[i] + end + + # Grab all the elements associated with this time. + pbs_v = @view pbs[idx...] + + # Now add this element's contribution to pbs_out. + _combine!(pbs_out, pbs_v, outcbands) + end + + return ProportionalBandSpectrum(pbs_out, outcbands) +end + +function _combine!(pbs_out::AbstractVector, pbs::AbstractVector{<:AbstractProportionalBandSpectrum}, outcbands::AbstractProportionalBands{NO,:center}) where {NO} + + # Get the time period for this collection of PBSs. + period = time_period(pbs) + + # Now start looping over each input PBS. + for pbs_in in pbs + + # Get the time scaler associated with this particular input PBS. + scaler = time_scaler(pbs_in, period) + + # Create a lazy version of the input proportional band spectrum using the output center bands. + pbs_in_lazy = lazy_pbs(pbs_in, outcbands) + + # Now add this element's contribution to output pbs. + pbs_out .+= pbs_in_lazy .* scaler + end + + return nothing end diff --git a/test/Project.toml b/test/Project.toml index 99bf7bf..296cf34 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,6 +1,13 @@ [deps] ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" +OffsetArrays = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" Polynomials = "f27b6e38-b328-58d1-80ce-0feddd5e7a45" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[compat] +ForwardDiff = "0.10.19" +JLD2 = "0.4.46" +OffsetArrays = "1.14.0" +Polynomials = "4.0.6" diff --git a/test/dfts.jl b/test/dfts.jl new file mode 100644 index 0000000..b122518 --- /dev/null +++ b/test/dfts.jl @@ -0,0 +1,119 @@ +using OffsetArrays: OffsetArray + +""" + dft_r2hc(x::AbstractVector) + +Calculate the real-input discrete Fourier transform, returning the result in the "half-complex" format. + +See +http://www.fftw.org/fftw3_doc/The-1d-Real_002ddata-DFT.html#The-1d-Real_002ddata-DFT +and http://www.fftw.org/fftw3_doc/The-Halfcomplex_002dformat-DFT.html for +details. + +Only use this for checking the derivatives of the FFT routines (should work fine, just slow). +""" +function dft_r2hc(x::AbstractVector) + # http://www.fftw.org/fftw3_doc/The-1d-Real_002ddata-DFT.html#The-1d-Real_002ddata-DFT + # http://www.fftw.org/fftw3_doc/The-Halfcomplex_002dformat-DFT.html + # So + # + # * we don't need the imaginary part of y_0 (which will be the first element in y, say, i=1) + # * if n is even, we don't need the imaginary part of y_{n/2} (which would be i = n/2+1) + # + # Now, the order is supposed to be like this (r for real, i for imaginary): + # + # * r_0, r_1, r_2, r_{n/2}, i_{(n+1)/2-1}, ..., i_2, i_1 + # + # But the docs say that they're still using the same old formula, which is: + # + # Y_k = Σ_{j=0}^{n-1} X_j exp(-2*π*i*j*k/n) + # + # (where i is sqrt(-1)). + n = length(x) + xo = OffsetArray(x, 0:n-1) + + y = similar(x) + yo = OffsetArray(y, 0:n-1) + + # Let's do k = 0 first. + yo[0] = sum(xo) + + # Now go from k = 1 to n/2 for the real parts. + T = eltype(x) + for k in 1:n÷2 + yo[k] = zero(T) + for j in 0:n-1 + # yo[k] += xo[j]*exp(-2*pi*sqrt(-1)*j*k/n) + yo[k] += xo[j]*cos(-2*pi*j*k/n) + end + end + + # Now go from 1 to (n+1)/2-1 for the imaginary parts. + for k in 1:(n+1)÷2-1 + yo[n-k] = zero(T) + for j in 0:n-1 + yo[n-k] += xo[j]*sin(-2*pi*j*k/n) + end + end + + return y +end + +""" + dft_hc2r(x::AbstractVector) + +Calculate the inverse discrete Fourier transform of a real-input DFT. + +This is the inverse of `dft_r2hc`, except for a factor of `N`, where `N` is the length of the input (and output), since FFTW computes an "unnormalized" FFT. + +See +http://www.fftw.org/fftw3_doc/The-1d-Real_002ddata-DFT.html#The-1d-Real_002ddata-DFT +and http://www.fftw.org/fftw3_doc/The-Halfcomplex_002dformat-DFT.html for +details. + +Only use this for checking the derivatives of the FFT routines (should work fine, just slow). +""" +function dft_hc2r(x::AbstractVector) + n = length(x) + xo = OffsetArray(x, 0:n-1) + + y = zero(x) + yo = OffsetArray(y, 0:n-1) + + j = 0 + for k in 0:n-1 + yo[k] += xo[j] + end + + # So, I need this loop to get r_1 to r_{n÷2} and i_{(n+1)÷2-1} to i_1. + # Let's say n is even. + # Maybe 8. + # So then n÷2 == 4 and (n+1)÷2-1 == 3. + # So x0 looks like this: + # + # r_0, r_1, r_2, r_3, r_4, i_3, i_2, i_1 + # + # If n is odd, say, 9, then n÷2 == 4 and (n+1)÷2-1 == 4, and x0 looks like this: + # + # r_0, r_1, r_2, r_3, r_4, i_4, i_3, i_2, i_1 + # + for j in 1:(n-1)÷2 + rj = xo[j] + ij = xo[n-j] + for k in 0:n-1 + yo[k] += 2*rj*cos(2*pi*j*k/n) - 2*ij*sin(2*pi*j*k/n) + end + end + + if iseven(n) + # Handle the Nyquist frequency. + j = n÷2 + rj = xo[j] + for k in 0:n-1 + yo[k] += rj*cos(2*pi*j*k/n) + end + end + + return y +end + diff --git a/test/runtests.jl b/test/runtests.jl index 4fde68b..a137550 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,15 +1,21 @@ using AcousticMetrics: p_ref -using AcousticMetrics: r2rfftfreq, rfft, rfft!, irfft, irfft!, RFFTCache, dft_r2hc, dft_hc2r +using AcousticMetrics: r2rfftfreq, rfft, rfft!, irfft, irfft!, RFFTCache using AcousticMetrics: PressureTimeHistory using AcousticMetrics: PressureSpectrumAmplitude, PressureSpectrumPhase, MSPSpectrumAmplitude, MSPSpectrumPhase, PowerSpectralDensityAmplitude, PowerSpectralDensityPhase -using AcousticMetrics: starttime, timestep, time, pressure, frequency, halfcomplex, OASPL -using AcousticMetrics: band_start, band_end +using AcousticMetrics: starttime, timestep, frequencystep, time, pressure, frequency, halfcomplex, OASPL, istonal +using AcousticMetrics: octave_fraction, band_start, band_end, cband_number +using AcousticMetrics: AbstractProportionalBands using AcousticMetrics: ExactOctaveCenterBands, ExactOctaveLowerBands, ExactOctaveUpperBands using AcousticMetrics: ExactThirdOctaveCenterBands, ExactThirdOctaveLowerBands, ExactThirdOctaveUpperBands using AcousticMetrics: ExactProportionalBands, lower_bands, center_bands, upper_bands -using AcousticMetrics: ProportionalBandSpectrum, ExactThirdOctaveSpectrum +using AcousticMetrics: AbstractProportionalBandSpectrum +using AcousticMetrics: LazyNBProportionalBandSpectrum, ProportionalBandSpectrum using AcousticMetrics: ApproximateOctaveBands, ApproximateOctaveCenterBands, ApproximateOctaveLowerBands, ApproximateOctaveUpperBands using AcousticMetrics: ApproximateThirdOctaveBands, ApproximateThirdOctaveCenterBands, ApproximateThirdOctaveLowerBands, ApproximateThirdOctaveUpperBands +using AcousticMetrics: combine +using AcousticMetrics: freq_scaler, time_period, time_scaler, has_observer_time, observer_time +using AcousticMetrics: ProportionalBandSpectrumWithTime +using AcousticMetrics: LazyPBSProportionalBandSpectrum, frequency_nb using AcousticMetrics: W_A using ForwardDiff using JLD2 @@ -17,6 +23,8 @@ using Polynomials: Polynomials using Random using Test +include("dfts.jl") + include(joinpath(@__DIR__, "gen_anopp2_data", "test_functions.jl")) @@ -183,8 +191,6 @@ end @test all(isapprox.(time(ap), t)) @test timestep(ap) ≈ dt @test starttime(ap) ≈ 0.0 - # ps = PressureSpectrum(ap) - # amp = amplitude(ps) amp = PressureSpectrumAmplitude(ap) phase = PressureSpectrumPhase(ap) freq_expected = [0.0, 1/T, 2/T, 3/T, 4/T, 5/T] @@ -213,12 +219,37 @@ end @test all(isapprox.(frequency(phase), freq_expected; atol=1e-12)) @test all(isapprox.(amp, amp_expected; atol=1e-12)) @test all(isapprox.(phase.*amp, phase_expected.*amp_expected; atol=1e-12)) + @test timestep(amp) ≈ dt + @test timestep(phase) ≈ dt + @test starttime(amp) ≈ 0.0 + @test starttime(phase) ≈ 0.0 + @test frequencystep(amp) ≈ freq_expected[2] + @test frequencystep(phase) ≈ freq_expected[2] + @test istonal(amp) == false + @test istonal(phase) == false # Make sure I can go from a PressureSpectrum to an PressureTimeHistory. ap_from_ps = PressureTimeHistory(amp) @test timestep(ap_from_ps) ≈ timestep(ap) @test starttime(ap_from_ps) ≈ starttime(ap) @test all(isapprox.(pressure(ap_from_ps), pressure(ap))) + + # Create a tonal version of the same spectrum. + # Nothing should be any different except the `IsTonal` parameter. + amp_tonal = PressureSpectrumAmplitude(halfcomplex(amp), timestep(amp), starttime(amp), true) + phase_tonal = PressureSpectrumPhase(halfcomplex(phase), timestep(phase), starttime(phase), true) + @test all(isapprox.(frequency(amp_tonal), freq_expected; atol=1e-12)) + @test all(isapprox.(frequency(phase_tonal), freq_expected; atol=1e-12)) + @test all(isapprox.(amp_tonal, amp_expected; atol=1e-12)) + @test all(isapprox.(phase_tonal.*amp_tonal, phase_expected.*amp_expected; atol=1e-12)) + @test timestep(amp_tonal) ≈ dt + @test timestep(phase_tonal) ≈ dt + @test starttime(amp_tonal) ≈ 0.0 + @test starttime(phase_tonal) ≈ 0.0 + @test frequencystep(amp_tonal) ≈ freq_expected[2] + @test frequencystep(phase_tonal) ≈ freq_expected[2] + @test istonal(amp_tonal) == true + @test istonal(phase_tonal) == true end end @testset "negative amplitudes" begin @@ -229,7 +260,9 @@ end t = (0:n-1).*dt p = f.(t) ap = PressureTimeHistory(p, dt) - # ps = PressureSpectrum(ap) + @test all(isapprox.(time(ap), t)) + @test timestep(ap) ≈ dt + @test starttime(ap) ≈ 0.0 amp = PressureSpectrumAmplitude(ap) phase = PressureSpectrumPhase(ap) freq_expected = [0.0, 1/T, 2/T, 3/T, 4/T, 5/T] @@ -258,12 +291,37 @@ end @test all(isapprox.(frequency(phase), freq_expected; atol=1e-12)) @test all(isapprox.(amp, amp_expected; atol=1e-12)) @test all(isapprox.(phase.*amp, phase_expected.*amp_expected; atol=1e-12)) + @test timestep(amp) ≈ dt + @test timestep(phase) ≈ dt + @test starttime(amp) ≈ 0.0 + @test starttime(phase) ≈ 0.0 + @test frequencystep(amp) ≈ freq_expected[2] + @test frequencystep(phase) ≈ freq_expected[2] + @test istonal(amp) == false + @test istonal(phase) == false # Make sure I can go from a PressureSpectrum to an PressureTimeHistory. ap_from_ps = PressureTimeHistory(amp) @test timestep(ap_from_ps) ≈ timestep(ap) @test starttime(ap_from_ps) ≈ starttime(ap) @test all(isapprox.(pressure(ap_from_ps), pressure(ap))) + + # Create a tonal version of the same spectrum. + # Nothing should be any different except the `IsTonal` parameter. + amp_tonal = PressureSpectrumAmplitude(halfcomplex(amp), timestep(amp), starttime(amp), true) + phase_tonal = PressureSpectrumPhase(halfcomplex(phase), timestep(phase), starttime(phase), true) + @test all(isapprox.(frequency(amp_tonal), freq_expected; atol=1e-12)) + @test all(isapprox.(frequency(phase_tonal), freq_expected; atol=1e-12)) + @test all(isapprox.(amp_tonal, amp_expected; atol=1e-12)) + @test all(isapprox.(phase_tonal.*amp_tonal, phase_expected.*amp_expected; atol=1e-12)) + @test timestep(amp_tonal) ≈ dt + @test timestep(phase_tonal) ≈ dt + @test starttime(amp_tonal) ≈ 0.0 + @test starttime(phase_tonal) ≈ 0.0 + @test frequencystep(amp_tonal) ≈ freq_expected[2] + @test frequencystep(phase_tonal) ≈ freq_expected[2] + @test istonal(amp_tonal) == true + @test istonal(phase_tonal) == true end end end @@ -281,7 +339,6 @@ end @test all(isapprox.(time(ap), t)) @test timestep(ap) ≈ dt @test starttime(ap) ≈ t0 - # ps = PressureSpectrum(ap) amp = PressureSpectrumAmplitude(ap) phase = PressureSpectrumPhase(ap) freq_expected = [0.0, 1/T, 2/T, 3/T, 4/T, 5/T] @@ -310,12 +367,37 @@ end @test all(isapprox.(frequency(phase), freq_expected; atol=1e-12)) @test all(isapprox.(amp, amp_expected; atol=1e-12)) @test all(isapprox.(phase, phase_expected; atol=1e-12)) + @test timestep(amp) ≈ dt + @test timestep(phase) ≈ dt + @test starttime(amp) ≈ t0 + @test starttime(phase) ≈ t0 + @test frequencystep(amp) ≈ freq_expected[2] + @test frequencystep(phase) ≈ freq_expected[2] + @test istonal(amp) == false + @test istonal(phase) == false # Make sure I can go from a PressureSpectrum to an PressureTimeHistory. ap_from_ps = PressureTimeHistory(amp) @test timestep(ap_from_ps) ≈ timestep(ap) @test starttime(ap_from_ps) ≈ starttime(ap) @test all(isapprox.(pressure(ap_from_ps), pressure(ap))) + + # Create a tonal version of the same spectrum. + # Nothing should be any different except the `IsTonal` parameter. + amp_tonal = PressureSpectrumAmplitude(halfcomplex(amp), timestep(amp), starttime(amp), true) + phase_tonal = PressureSpectrumPhase(halfcomplex(phase), timestep(phase), starttime(phase), true) + @test all(isapprox.(frequency(amp_tonal), freq_expected; atol=1e-12)) + @test all(isapprox.(frequency(phase_tonal), freq_expected; atol=1e-12)) + @test all(isapprox.(amp_tonal, amp_expected; atol=1e-12)) + @test all(isapprox.(phase_tonal.*amp_tonal, phase_expected.*amp_expected; atol=1e-12)) + @test timestep(amp_tonal) ≈ dt + @test timestep(phase_tonal) ≈ dt + @test starttime(amp_tonal) ≈ t0 + @test starttime(phase_tonal) ≈ t0 + @test frequencystep(amp_tonal) ≈ freq_expected[2] + @test frequencystep(phase_tonal) ≈ freq_expected[2] + @test istonal(amp_tonal) == true + @test istonal(phase_tonal) == true end end end @@ -330,6 +412,9 @@ end t = (0:n-1).*dt p = f.(t) ap = PressureTimeHistory(p, dt) + @test all(isapprox.(time(ap), t)) + @test timestep(ap) ≈ dt + @test starttime(ap) ≈ 0.0 amp = MSPSpectrumAmplitude(ap) phase = MSPSpectrumPhase(ap) freq_expected = [0.0, 1/T, 2/T, 3/T, 4/T, 5/T] @@ -358,30 +443,55 @@ end @test all(isapprox.(frequency(phase), freq_expected; atol=1e-12)) @test all(isapprox.(amp, amp_expected; atol=1e-12)) @test all(isapprox.(phase.*amp, phase_expected.*amp_expected; atol=1e-12)) + @test timestep(amp) ≈ dt + @test timestep(phase) ≈ dt + @test starttime(amp) ≈ 0.0 + @test starttime(phase) ≈ 0.0 + @test frequencystep(amp) ≈ freq_expected[2] + @test frequencystep(phase) ≈ freq_expected[2] + @test istonal(amp) == false + @test istonal(phase) == false # Make sure I can convert a mean-squared pressure to a pressure spectrum. psamp = PressureSpectrumAmplitude(amp) - amp_expected = similar(amp) - amp_expected[1] = 6 - amp_expected[2] = 8 - amp_expected[3] = 2.5 - amp_expected[4] = 9 - amp_expected[5] = 0.5 + psamp_expected = similar(amp) + psamp_expected[1] = 6 + psamp_expected[2] = 8 + psamp_expected[3] = 2.5 + psamp_expected[4] = 9 + psamp_expected[5] = 0.5 # Handle the Nyquist frequency (kinda tricky). There isn't really a # Nyquist frequency for the odd input length case. if n == 10 - amp_expected[6] = 3*cos(0.2) + psamp_expected[6] = 3*cos(0.2) else - amp_expected[6] = 3 + psamp_expected[6] = 3 end @test all(isapprox.(frequency(psamp), freq_expected; atol=1e-12)) - @test all(isapprox.(psamp, amp_expected; atol=1e-12)) + @test all(isapprox.(psamp, psamp_expected; atol=1e-12)) # Make sure I can convert a mean-squared pressure to the acoustic pressure. ap_from_msp = PressureTimeHistory(amp) @test timestep(ap_from_msp) ≈ timestep(ap) @test starttime(ap_from_msp) ≈ starttime(ap) @test all(isapprox.(pressure(ap_from_msp), pressure(ap))) + + # Create a tonal version of the same spectrum. + # Nothing should be any different except the `IsTonal` parameter. + amp_tonal = MSPSpectrumAmplitude(halfcomplex(amp), timestep(amp), starttime(amp), true) + phase_tonal = MSPSpectrumPhase(halfcomplex(phase), timestep(phase), starttime(phase), true) + @test all(isapprox.(frequency(amp_tonal), freq_expected; atol=1e-12)) + @test all(isapprox.(frequency(phase_tonal), freq_expected; atol=1e-12)) + @test all(isapprox.(amp_tonal, amp_expected; atol=1e-12)) + @test all(isapprox.(phase_tonal.*amp_tonal, phase_expected.*amp_expected; atol=1e-12)) + @test timestep(amp_tonal) ≈ dt + @test timestep(phase_tonal) ≈ dt + @test starttime(amp_tonal) ≈ 0.0 + @test starttime(phase_tonal) ≈ 0.0 + @test frequencystep(amp_tonal) ≈ freq_expected[2] + @test frequencystep(phase_tonal) ≈ freq_expected[2] + @test istonal(amp_tonal) == true + @test istonal(phase_tonal) == true end end end @@ -395,6 +505,9 @@ end t = t0 .+ (0:n-1).*dt p = f.(t) ap = PressureTimeHistory(p, dt, t0) + @test all(isapprox.(time(ap), t)) + @test timestep(ap) ≈ dt + @test starttime(ap) ≈ t0 amp = MSPSpectrumAmplitude(ap) phase = MSPSpectrumPhase(ap) freq_expected = [0.0, 1/T, 2/T, 3/T, 4/T, 5/T] @@ -425,34 +538,59 @@ end @test all(isapprox.(frequency(phase), freq_expected; atol=1e-12)) @test all(isapprox.(amp, amp_expected; atol=1e-12)) @test all(isapprox.(phase, phase_expected; atol=1e-12)) + @test timestep(amp) ≈ dt + @test timestep(phase) ≈ dt + @test starttime(amp) ≈ t0 + @test starttime(phase) ≈ t0 + @test frequencystep(amp) ≈ freq_expected[2] + @test frequencystep(phase) ≈ freq_expected[2] + @test istonal(amp) == false + @test istonal(phase) == false # Make sure I can convert a mean-squared pressure to a pressure spectrum. psamp = PressureSpectrumAmplitude(amp) - amp_expected = similar(psamp) - amp_expected[1] = 6 - amp_expected[2] = 8 - amp_expected[3] = 2.5 - amp_expected[4] = 9 - amp_expected[5] = 0.5 + psamp_expected = similar(psamp) + psamp_expected[1] = 6 + psamp_expected[2] = 8 + psamp_expected[3] = 2.5 + psamp_expected[4] = 9 + psamp_expected[5] = 0.5 # Handle the Nyquist frequency (kinda tricky). There isn't really a # Nyquist frequency for the odd input length case. if n == 10 # The `t0` term pushes the cosine below zero, which messes # up the test. Hmm... what's the right thing to do here? # Well, what should the phase and amplitude be? - # amp_expected[6] = 3*cos(5*2*pi/T*t0 + 0.2) - amp_expected[6] = abs(3*cos(5*2*pi/T*t0 + 0.2)) + # psamp_expected[6] = 3*cos(5*2*pi/T*t0 + 0.2) + psamp_expected[6] = abs(3*cos(5*2*pi/T*t0 + 0.2)) else - amp_expected[6] = 3 + psamp_expected[6] = 3 end @test all(isapprox.(frequency(psamp), freq_expected; atol=1e-12)) - @test all(isapprox.(psamp, amp_expected; atol=1e-12)) + @test all(isapprox.(psamp, psamp_expected; atol=1e-12)) # Make sure I can convert a mean-squared pressure to the acoustic pressure. ap_from_msp = PressureTimeHistory(amp) @test timestep(ap_from_msp) ≈ timestep(ap) @test starttime(ap_from_msp) ≈ starttime(ap) @test all(isapprox.(pressure(ap_from_msp), pressure(ap))) + + # Create a tonal version of the same spectrum. + # Nothing should be any different except the `IsTonal` parameter. + amp_tonal = MSPSpectrumAmplitude(halfcomplex(amp), timestep(amp), starttime(amp), true) + phase_tonal = MSPSpectrumPhase(halfcomplex(phase), timestep(phase), starttime(phase), true) + @test all(isapprox.(frequency(amp_tonal), freq_expected; atol=1e-12)) + @test all(isapprox.(frequency(phase_tonal), freq_expected; atol=1e-12)) + @test all(isapprox.(amp_tonal, amp_expected; atol=1e-12)) + @test all(isapprox.(phase_tonal.*amp_tonal, phase_expected.*amp_expected; atol=1e-12)) + @test timestep(amp_tonal) ≈ dt + @test timestep(phase_tonal) ≈ dt + @test starttime(amp_tonal) ≈ t0 + @test starttime(phase_tonal) ≈ t0 + @test frequencystep(amp_tonal) ≈ freq_expected[2] + @test frequencystep(phase_tonal) ≈ freq_expected[2] + @test istonal(amp_tonal) == true + @test istonal(phase_tonal) == true end end end @@ -496,6 +634,9 @@ end t = (0:n-1).*dt p = f.(t) ap = PressureTimeHistory(p, dt) + @test all(isapprox.(time(ap), t)) + @test timestep(ap) ≈ dt + @test starttime(ap) ≈ 0.0 amp = PowerSpectralDensityAmplitude(ap) phase = PowerSpectralDensityPhase(ap) freq_expected = [0.0, 1/T, 2/T, 3/T, 4/T, 5/T] @@ -524,30 +665,44 @@ end @test all(isapprox.(frequency(phase), freq_expected; atol=1e-12)) @test all(isapprox.(amp, amp_expected; atol=1e-12)) @test all(isapprox.(phase.*amp, phase_expected.*amp_expected; atol=1e-12)) + @test timestep(amp) ≈ dt + @test timestep(phase) ≈ dt + @test starttime(amp) ≈ 0.0 + @test starttime(phase) ≈ 0.0 + @test frequencystep(amp) ≈ freq_expected[2] + @test frequencystep(phase) ≈ freq_expected[2] + @test istonal(amp) == false + @test istonal(phase) == false # Make sure I can convert a PSD to a pressure spectrum. psamp = PressureSpectrumAmplitude(amp) - amp_expected = similar(psamp) - amp_expected[1] = 6 - amp_expected[2] = 8 - amp_expected[3] = 2.5 - amp_expected[4] = 9 - amp_expected[5] = 0.5 + psamp_expected = similar(psamp) + psamp_expected[1] = 6 + psamp_expected[2] = 8 + psamp_expected[3] = 2.5 + psamp_expected[4] = 9 + psamp_expected[5] = 0.5 # Handle the Nyquist frequency (kinda tricky). There isn't really a # Nyquist frequency for the odd input length case. if n == 10 - amp_expected[6] = 3*cos(0.2) + psamp_expected[6] = 3*cos(0.2) else - amp_expected[6] = 3 + psamp_expected[6] = 3 end @test all(isapprox.(frequency(psamp), freq_expected; atol=1e-12)) - @test all(isapprox.(psamp, amp_expected; atol=1e-12)) + @test all(isapprox.(psamp, psamp_expected; atol=1e-12)) # Make sure I can convert a PSD to the acoustic pressure. ap_from_psd = PressureTimeHistory(amp) @test timestep(ap_from_psd) ≈ timestep(ap) @test starttime(ap_from_psd) ≈ starttime(ap) @test all(isapprox.(pressure(ap_from_psd), pressure(ap))) + + # I shouldn't be able to create a PSD from a tonal spectrum. + amp_tonal = PressureSpectrumAmplitude(halfcomplex(amp), timestep(amp), starttime(amp), true) + phase_tonal = PressureSpectrumPhase(halfcomplex(phase), timestep(phase), starttime(phase), true) + @test_throws ArgumentError PowerSpectralDensityAmplitude(amp_tonal) + # @test_throws ArgumentError PowerSpectralDensityPhase(phase_tonal) end end end @@ -562,6 +717,9 @@ end t = t0 .+ (0:n-1).*dt p = f.(t) ap = PressureTimeHistory(p, dt, t0) + @test all(isapprox.(time(ap), t)) + @test timestep(ap) ≈ dt + @test starttime(ap) ≈ t0 amp = PowerSpectralDensityAmplitude(ap) phase = PowerSpectralDensityPhase(ap) freq_expected = [0.0, 1/T, 2/T, 3/T, 4/T, 5/T] @@ -592,15 +750,23 @@ end @test all(isapprox.(frequency(phase), freq_expected; atol=1e-12)) @test all(isapprox.(amp, amp_expected; atol=1e-12)) @test all(isapprox.(phase, phase_expected; atol=1e-12)) + @test timestep(amp) ≈ dt + @test timestep(phase) ≈ dt + @test starttime(amp) ≈ t0 + @test starttime(phase) ≈ t0 + @test frequencystep(amp) ≈ freq_expected[2] + @test frequencystep(phase) ≈ freq_expected[2] + @test istonal(amp) == false + @test istonal(phase) == false # Make sure I can convert a PSD to a pressure spectrum. psamp = PressureSpectrumAmplitude(amp) - amp_expected = similar(psamp) - amp_expected[1] = 6 - amp_expected[2] = 8 - amp_expected[3] = 2.5 - amp_expected[4] = 9 - amp_expected[5] = 0.5 + psamp_expected = similar(psamp) + psamp_expected[1] = 6 + psamp_expected[2] = 8 + psamp_expected[3] = 2.5 + psamp_expected[4] = 9 + psamp_expected[5] = 0.5 # Handle the Nyquist frequency (kinda tricky). There isn't really a # Nyquist frequency for the odd input length case. if n == 10 @@ -608,18 +774,25 @@ end # up the test. Hmm... what's the right thing to do here? # Well, what should the phase and amplitude be? # amp_expected[6] = 3*cos(5*2*pi/T*t0 + 0.2) - amp_expected[6] = abs(3*cos(5*2*pi/T*t0 + 0.2)) + psamp_expected[6] = abs(3*cos(5*2*pi/T*t0 + 0.2)) else - amp_expected[6] = 3 + psamp_expected[6] = 3 end @test all(isapprox.(frequency(psamp), freq_expected; atol=1e-12)) - @test all(isapprox.(psamp, amp_expected; atol=1e-12)) + @test all(isapprox.(psamp, psamp_expected; atol=1e-12)) # Make sure I can convert a PSD to the acoustic pressure. ap_from_psd = PressureTimeHistory(amp) @test timestep(ap_from_psd) ≈ timestep(ap) @test starttime(ap_from_psd) ≈ starttime(ap) @test all(isapprox.(pressure(ap_from_psd), pressure(ap))) + + # I shouldn't be able to create a PSD from a tonal spectrum. + # But I actually can creeate a PSD phase, since the PSD phase is the same as the pressure and MSP phase. + amp_tonal = PressureSpectrumAmplitude(halfcomplex(amp), timestep(amp), starttime(amp), true) + phase_tonal = PressureSpectrumPhase(halfcomplex(phase), timestep(phase), starttime(phase), true) + @test_throws ArgumentError PowerSpectralDensityAmplitude(amp_tonal) + # @test_throws ArgumentError PowerSpectralDensityPhase(phase_tonal) end end end @@ -655,79 +828,191 @@ end @testset "Proportional Band Spectrum" begin @testset "exact octave" begin - bands = ExactOctaveCenterBands(6, 16) - bands_expected = [62.5, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0, 16000.0, 32000.0, 64000.0] - @test all(isapprox.(bands, bands_expected)) + @testset "standard" begin + bands = ExactOctaveCenterBands(6, 16) + @test octave_fraction(bands) == 1 + + bands_expected = [62.5, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0, 16000.0, 32000.0, 64000.0] + @test all(isapprox.(bands, bands_expected)) + + @test_throws BoundsError bands[0] + @test_throws BoundsError bands[12] + + bands_9_to_11 = ExactOctaveCenterBands(9, 11) + @test all(isapprox.(bands_9_to_11, bands_expected[4:6])) + + @test_throws BoundsError bands_9_to_11[0] + @test_throws BoundsError bands_9_to_11[4] + + @test_throws ArgumentError ExactOctaveCenterBands(5, 4) + + lbands = ExactOctaveLowerBands(6, 16) + @test octave_fraction(lbands) == 1 + @test all((log2.(bands) .- log2.(lbands)) .≈ 1/2) + + ubands = ExactOctaveUpperBands(6, 16) + @test octave_fraction(ubands) == 1 + @test all((log2.(ubands) .- log2.(bands)) .≈ 1/2) + + @test all((log2.(ubands) .- log2.(lbands)) .≈ 1) + + cbands = ExactOctaveCenterBands(700.0, 22000.0) + @test octave_fraction(cbands) == 1 + @test cbands.bstart == 9 + @test cbands.bend == 14 + + lbands = ExactOctaveLowerBands(700.0, 22000.0) + @test lbands.bstart == 9 + @test lbands.bend == 14 + + ubands = ExactOctaveUpperBands(700.0, 22000.0) + @test ubands.bstart == 9 + @test ubands.bend == 14 + + # Test the `_cband_exact` routine, which goes from an exact centerband frequency to the band number. + for (i, cband) in enumerate(cbands) + @test cband_number(cbands, cband) == (band_start(cbands) + i - 1) + end + end - @test_throws BoundsError bands[0] - @test_throws BoundsError bands[12] + @testset "scaler argument" begin + for scaler in [0.1, 0.5, 1.0, 1.5, 2.0] + bands = ExactOctaveCenterBands(6, 16, scaler) + bands_expected = scaler .* [62.5, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0, 16000.0, 32000.0, 64000.0] + @test all(isapprox.(bands, bands_expected)) - bands_9_to_11 = ExactOctaveCenterBands(9, 11) - @test all(isapprox.(bands_9_to_11, bands_expected[4:6])) + @test_throws BoundsError bands[0] + @test_throws BoundsError bands[12] - @test_throws BoundsError bands_9_to_11[0] - @test_throws BoundsError bands_9_to_11[4] + bands_9_to_11 = ExactOctaveCenterBands(9, 11, scaler) + @test all(isapprox.(bands_9_to_11, bands_expected[4:6])) - @test_throws ArgumentError ExactOctaveCenterBands(5, 4) + @test_throws BoundsError bands_9_to_11[0] + @test_throws BoundsError bands_9_to_11[4] - lbands = ExactOctaveLowerBands(6, 16) - @test all((log2.(bands) .- log2.(lbands)) .≈ 1/2) + @test_throws ArgumentError ExactOctaveCenterBands(5, 4, scaler) - ubands = ExactOctaveUpperBands(6, 16) - @test all((log2.(ubands) .- log2.(bands)) .≈ 1/2) + lbands = ExactOctaveLowerBands(6, 16, scaler) + @test all((log2.(bands) .- log2.(lbands)) .≈ 1/2) - @test all((log2.(ubands) .- log2.(lbands)) .≈ 1) + ubands = ExactOctaveUpperBands(6, 16, scaler) + @test all((log2.(ubands) .- log2.(bands)) .≈ 1/2) - cbands = ExactOctaveCenterBands(700.0, 22000.0) - @test cbands.bstart == 9 - @test cbands.bend == 14 + @test all((log2.(ubands) .- log2.(lbands)) .≈ 1) - lbands = ExactOctaveLowerBands(700.0, 22000.0) - @test lbands.bstart == 9 - @test lbands.bend == 14 + cbands = ExactOctaveCenterBands(700.0*scaler, 22000.0*scaler, scaler) + @test cbands.bstart == 9 + @test cbands.bend == 14 - ubands = ExactOctaveUpperBands(700.0, 22000.0) - @test ubands.bstart == 9 - @test ubands.bend == 14 + lbands = ExactOctaveLowerBands(700.0*scaler, 22000.0*scaler, scaler) + @test lbands.bstart == 9 + @test lbands.bend == 14 + + ubands = ExactOctaveUpperBands(700.0*scaler, 22000.0*scaler, scaler) + @test ubands.bstart == 9 + @test ubands.bend == 14 + + # Test the `_cband_exact` routine, which goes from an exact centerband frequency to the band number. + for (i, cband) in enumerate(cbands) + @test cband_number(cbands, cband) == (band_start(cbands) + i - 1) + end + end + end end @testset "exact 1/3-octave" begin - bands = ExactThirdOctaveCenterBands(17, 40) - # These are just from the ANOPP2 manual. - bands_expected_all = [49.61, 62.50, 78.75, 99.21, 125.00, 157.49, 198.43, 250.00, 314.98, 396.85, 500.00, 629.96, 793.70, 1000.0, 1259.92, 1587.40, 2000.00, 2519.84, 3174.80, 4000.00, 5039.68, 6349.60, 8000.00, 10079.37] - @test all(isapprox.(bands, bands_expected_all; atol=0.005)) + @testset "standard" begin + bands = ExactThirdOctaveCenterBands(17, 40) + @test octave_fraction(bands) == 3 + # These are just from the ANOPP2 manual. + bands_expected_all = [49.61, 62.50, 78.75, 99.21, 125.00, 157.49, 198.43, 250.00, 314.98, 396.85, 500.00, 629.96, 793.70, 1000.0, 1259.92, 1587.40, 2000.00, 2519.84, 3174.80, 4000.00, 5039.68, 6349.60, 8000.00, 10079.37] + @test all(isapprox.(bands, bands_expected_all; atol=0.005)) + + @test_throws BoundsError bands[0] + @test_throws BoundsError bands[25] + + bands_30_to_38 = ExactThirdOctaveCenterBands(30, 38) + @test all(isapprox.(bands_30_to_38, bands_expected_all[14:end-2]; atol=0.005)) + + @test_throws BoundsError bands_30_to_38[0] + @test_throws BoundsError bands_30_to_38[10] + + @test_throws ArgumentError ExactThirdOctaveCenterBands(5, 4) + + lbands = ExactThirdOctaveLowerBands(17, 40) + @test octave_fraction(lbands) == 3 + @test all((log2.(bands) .- log2.(lbands)) .≈ 1/(2*3)) + + ubands = ExactThirdOctaveUpperBands(17, 40) + @test octave_fraction(ubands) == 3 + @test all((log2.(ubands) .- log2.(bands)) .≈ 1/(2*3)) + + @test all((log2.(ubands) .- log2.(lbands)) .≈ 1/3) + + cbands = ExactThirdOctaveCenterBands(332.0, 7150.0) + @test octave_fraction(cbands) == 3 + @test cbands.bstart == 25 + @test cbands.bend == 39 + + lbands = ExactThirdOctaveLowerBands(332.0, 7150.0) + @test lbands.bstart == 25 + @test lbands.bend == 39 + + ubands = ExactThirdOctaveUpperBands(332.0, 7150.0) + @test ubands.bstart == 25 + @test ubands.bend == 39 + + # Test the `cband_number` routine, which goes from an exact centerband frequency to the band number. + for (i, cband) in enumerate(cbands) + @test cband_number(cbands, cband) == (band_start(cbands) + i - 1) + end + end + + @testset "scaler argument" begin + for scaler in [0.1, 0.5, 1.0, 1.5, 2.0] + bands = ExactThirdOctaveCenterBands(17, 40, scaler) + # These are just from the ANOPP2 manual. + bands_expected_all = scaler .* [49.61, 62.50, 78.75, 99.21, 125.00, 157.49, 198.43, 250.00, 314.98, 396.85, 500.00, 629.96, 793.70, 1000.0, 1259.92, 1587.40, 2000.00, 2519.84, 3174.80, 4000.00, 5039.68, 6349.60, 8000.00, 10079.37] + @test all(isapprox.(bands, bands_expected_all; atol=0.01)) + + @test_throws BoundsError bands[0] + @test_throws BoundsError bands[25] - @test_throws BoundsError bands[0] - @test_throws BoundsError bands[25] + bands_30_to_38 = ExactThirdOctaveCenterBands(30, 38, scaler) + @test all(isapprox.(bands_30_to_38, bands_expected_all[14:end-2]; atol=0.01)) - bands_30_to_38 = ExactThirdOctaveCenterBands(30, 38) - @test all(isapprox.(bands_30_to_38, bands_expected_all[14:end-2]; atol=0.005)) + @test_throws BoundsError bands_30_to_38[0] + @test_throws BoundsError bands_30_to_38[10] - @test_throws BoundsError bands_30_to_38[0] - @test_throws BoundsError bands_30_to_38[10] + @test_throws ArgumentError ExactThirdOctaveCenterBands(5, 4, scaler) - @test_throws ArgumentError ExactThirdOctaveCenterBands(5, 4) + lbands = ExactThirdOctaveLowerBands(17, 40, scaler) + @test all((log2.(bands) .- log2.(lbands)) .≈ 1/(2*3)) - lbands = ExactThirdOctaveLowerBands(17, 40) - @test all((log2.(bands) .- log2.(lbands)) .≈ 1/(2*3)) + ubands = ExactThirdOctaveUpperBands(17, 40, scaler) + @test all((log2.(ubands) .- log2.(bands)) .≈ 1/(2*3)) - ubands = ExactThirdOctaveUpperBands(17, 40) - @test all((log2.(ubands) .- log2.(bands)) .≈ 1/(2*3)) + @test all((log2.(ubands) .- log2.(lbands)) .≈ 1/3) - @test all((log2.(ubands) .- log2.(lbands)) .≈ 1/3) + cbands = ExactThirdOctaveCenterBands(332.0*scaler, 7150.0*scaler, scaler) + @test cbands.bstart == 25 + @test cbands.bend == 39 - cbands = ExactThirdOctaveCenterBands(332.0, 7150.0) - @test cbands.bstart == 25 - @test cbands.bend == 39 + lbands = ExactThirdOctaveLowerBands(332.0*scaler, 7150.0*scaler, scaler) + @test lbands.bstart == 25 + @test lbands.bend == 39 - lbands = ExactThirdOctaveLowerBands(332.0, 7150.0) - @test lbands.bstart == 25 - @test lbands.bend == 39 + ubands = ExactThirdOctaveUpperBands(332.0*scaler, 7150.0*scaler, scaler) + @test ubands.bstart == 25 + @test ubands.bend == 39 - ubands = ExactThirdOctaveUpperBands(332.0, 7150.0) - @test ubands.bstart == 25 - @test ubands.bend == 39 + # Test the `cband_number` routine, which goes from an exact centerband frequency to the band number. + for (i, cband) in enumerate(cbands) + @test cband_number(cbands, cband) == (band_start(cbands) + i - 1) + end + end + end @testset "not-so-narrow narrowband spectrum" begin T = 1/1000.0 @@ -741,7 +1026,15 @@ end p = f.(t) ap = PressureTimeHistory(p, dt) psd = PowerSpectralDensityAmplitude(ap) - pbs = ProportionalBandSpectrum(ExactProportionalBands{3}, psd) + pbs = LazyNBProportionalBandSpectrum(ExactProportionalBands{3}, psd) + + # Creating a non-lazy version of the PBS should give the same stuff as the lazy version. + pbs_non_lazy = ProportionalBandSpectrum(typeof(center_bands(pbs)), center_bands(pbs)[begin], pbs) + @test all(pbs_non_lazy .≈ pbs) + @test lower_bands(pbs_non_lazy) === lower_bands(pbs) + @test center_bands(pbs_non_lazy) === center_bands(pbs) + @test upper_bands(pbs_non_lazy) === upper_bands(pbs) + # So, this should have non-zero stuff at 1000 Hz, 2000 Hz, 3000 Hz, 4000 Hz, 5000 Hz. # And that means that, say, the 1000 Hz signal will exend from 500 # Hz to 1500 Hz. @@ -784,6 +1077,37 @@ end @test pbs[10] ≈ 0.5*0.5^2/df*(ubands[10] - lbands[10]) # Last one is wierd because of the Nyquist frequency. @test pbs[11] ≈ 0.5*0.5^2/df*(4500 - lbands[11]) + (3*cos(0.2))^2/df*(5500 - 4500) + + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(psd, center_bands(pbs)) + @test all(pbs_init_cbands .≈ pbs) + + # Now, what if `istonal==true`? + # Then the narrowband frequencies are thin, and so each narrowband frequency can only show up in one proportional band each. + tonal = true + msp_tonal = MSPSpectrumAmplitude(ap, tonal) + # Using `istonal=true` shouldn't be any different than `istonal=false`. + @test all(msp_tonal .≈ MSPSpectrumAmplitude(psd)) + # Now get the PBS and check it. + pbs_tonal = LazyNBProportionalBandSpectrum(ExactProportionalBands{3}, msp_tonal) + # Check that we end up with the proportional bands we expect: + cbands_tonal = center_bands(pbs_tonal) + band_start(cbands_tonal) == 30 + band_end(cbands_tonal) == 37 + # Now check that we have the right answer for the PBS. + @test pbs_tonal[1] ≈ 0.5*8^2 # 1000 Hz + @test pbs_tonal[2] ≈ 0 + @test pbs_tonal[3] ≈ 0 + @test pbs_tonal[4] ≈ 0.5*2.5^2 # 2000 Hz + @test pbs_tonal[5] ≈ 0 + @test pbs_tonal[6] ≈ 0.5*9^2 # 3000 Hz + @test pbs_tonal[7] ≈ 0.5*0.5^2 # 4000 Hz + # Last one is wierd because of the Nyquist frequency. + @test pbs_tonal[8] ≈ (3*cos(0.2))^2 # 5000 Hz + + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(msp_tonal, center_bands(pbs_tonal)) + @test all(pbs_init_cbands .≈ pbs_tonal) end @testset "narrowband spectrum, one narrowband per proportional band" begin @@ -799,7 +1123,15 @@ end p = f.(t) ap = PressureTimeHistory(p, dt) psd = PowerSpectralDensityAmplitude(ap) - pbs = ProportionalBandSpectrum(ExactProportionalBands{3}, psd) + pbs = LazyNBProportionalBandSpectrum(ExactProportionalBands{3}, psd) + + # Creating a non-lazy version of the PBS should give the same stuff as the lazy version. + pbs_non_lazy = ProportionalBandSpectrum(typeof(center_bands(pbs)), center_bands(pbs)[begin], pbs) + @test all(pbs_non_lazy .≈ pbs) + @test lower_bands(pbs_non_lazy) === lower_bands(pbs) + @test center_bands(pbs_non_lazy) === center_bands(pbs) + @test upper_bands(pbs_non_lazy) === upper_bands(pbs) + lbands = lower_bands(pbs) ubands = upper_bands(pbs) psd_freq = frequency(psd) @@ -831,6 +1163,30 @@ end end end + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(psd, center_bands(pbs)) + @test all(pbs_init_cbands .≈ pbs) + + # So, for this example, I only have non-zero stuff at 1000 Hz, 2000 Hz, 3000 Hz. + # But the lowest non-zero frequency is 50 Hz, highest is 3200 Hz. + istonal = true + msp_tonal = MSPSpectrumAmplitude(ap, istonal) + pbs_tonal = LazyNBProportionalBandSpectrum(ExactProportionalBands{3}, msp_tonal) + cbands_tonal = center_bands(pbs_tonal) + @test band_start(cbands_tonal) == 17 + @test band_end(cbands_tonal) == 35 + for (i, amp) in enumerate(pbs_tonal) + if i ∉ [14, 17, 19] + @test isapprox(amp, 0; atol=1e-12) + end + end + @test pbs_tonal[14] ≈ 0.5*8^2 + @test pbs_tonal[17] ≈ 0.5*2.5^2 + @test pbs_tonal[19] ≈ 0.5*9^2 + + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(msp_tonal, center_bands(pbs_tonal)) + @test all(pbs_init_cbands .≈ pbs_tonal) end @testset "narrowband spectrum, many narrowbands per proportional band" begin @@ -840,7 +1196,17 @@ end df_nb = (freq_max_nb - freq_min_nb)/(nfreq_nb - 1) f_nb = freq_min_nb .+ (0:(nfreq_nb-1)).*df_nb psd = psd_func.(f_nb) - pbs = ProportionalBandSpectrum(ExactProportionalBands{3}, freq_min_nb, df_nb, psd) + # pbs = LazyNBProportionalBandSpectrum(ExactProportionalBands{3}, freq_min_nb, df_nb, psd) + msp = psd .* df_nb + pbs = LazyNBProportionalBandSpectrum(ExactProportionalBands{3}, freq_min_nb, df_nb, msp) + + # Creating a non-lazy version of the PBS should give the same stuff as the lazy version. + pbs_non_lazy = ProportionalBandSpectrum(typeof(center_bands(pbs)), center_bands(pbs)[begin], pbs) + @test all(pbs_non_lazy .≈ pbs) + @test lower_bands(pbs_non_lazy) === lower_bands(pbs) + @test center_bands(pbs_non_lazy) === center_bands(pbs) + @test upper_bands(pbs_non_lazy) === upper_bands(pbs) + cbands = center_bands(pbs) pbs_level = @. 10*log10(pbs/p_ref^2) @@ -854,6 +1220,132 @@ end @test isapprox(pbs_level[j], a2_pbs[i]; atol=1e-2) end + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(freq_min_nb, df_nb, msp, center_bands(pbs)) + @test all(pbs_init_cbands .≈ pbs) + + # Let's create a tonal MSP. + scaler = 1 + tonal = true + pbs_tonal = LazyNBProportionalBandSpectrum(ExactProportionalBands{3}, freq_min_nb, df_nb, msp, scaler, tonal) + # So, the narrowband frequencies go from 55.0 Hz to 1950 Hz. + # Let's check that. + cbands_tonal = center_bands(pbs_tonal) + @test band_start(cbands_tonal) == 17 + @test band_end(cbands_tonal) == 33 + + # Now, this really isn't a tonal spectrum—it's actually very broadband. + # But I should still be able to check it. + # I need to indentify which narrowbands are in which proportional band. + lbands = lower_bands(pbs_tonal) + ubands = upper_bands(pbs_tonal) + for (lband, uband, amp) in zip(lbands, ubands, pbs_tonal) + # First index we want in f_nb is the one that is greater than or equal to lband. + istart = searchsortedfirst(f_nb, lband) + # Last index we want in f_nb is th one that is less than or equal to uband. + iend = searchsortedlast(f_nb, uband) + # Now check that we get the right answer. + @test sum(msp[istart:iend]) ≈ amp + end + + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(freq_min_nb, df_nb, msp, center_bands(pbs_tonal), tonal) + @test all(pbs_init_cbands .≈ pbs_tonal) + end + + @testset "narrowband spectrum, many narrowbands per proportional band, scaled frequency" begin + # Create a PBS with the standard frequencies. + nfreq_nb = 800 + freq_min_nb = 55.0 + freq_max_nb = 1950.0 + df_nb = (freq_max_nb - freq_min_nb)/(nfreq_nb - 1) + f_nb = freq_min_nb .+ (0:(nfreq_nb-1)).*df_nb + psd = psd_func.(f_nb) + # pbs = LazyNBProportionalBandSpectrum(ExactProportionalBands{3}, freq_min_nb, df_nb, psd) + msp = psd .* df_nb + pbs = LazyNBProportionalBandSpectrum(ExactProportionalBands{3}, freq_min_nb, df_nb, msp) + + # Creating a non-lazy version of the PBS should give the same stuff as the lazy version. + pbs_non_lazy = ProportionalBandSpectrum(typeof(center_bands(pbs)), center_bands(pbs)[begin], pbs) + @test all(pbs_non_lazy .≈ pbs) + @test lower_bands(pbs_non_lazy) === lower_bands(pbs) + @test center_bands(pbs_non_lazy) === center_bands(pbs) + @test upper_bands(pbs_non_lazy) === upper_bands(pbs) + + for scaler in [0.1, 0.5, 1.0, 1.5, 2.0] + # Now create another PBS, but with the scaled frequency bands, same psd. + freq_min_nb_scaled = 55.0*scaler + freq_max_nb_scaled = 1950.0*scaler + df_nb_scaled = (freq_max_nb_scaled - freq_min_nb_scaled)/(nfreq_nb - 1) + f_nb_scaled = freq_min_nb_scaled .+ (0:(nfreq_nb-1)).*df_nb_scaled + # pbs_scaled = LazyNBProportionalBandSpectrum(ExactProportionalBands{3}, freq_min_nb_scaled, df_nb_scaled, psd, scaler) + # pbs_scaled = LazyNBProportionalBandSpectrum(ExactProportionalBands{3}, freq_min_nb_scaled, df_nb_scaled, msp, scaler) + # msp_scaled = msp ./ df_nb .* df_nb_scaled + # If we want the same psd, we need to adjust for the new narrowband frequency bin width. + msp_scaled = psd .* df_nb_scaled + pbs_scaled = LazyNBProportionalBandSpectrum(ExactProportionalBands{3}, freq_min_nb_scaled, df_nb_scaled, msp_scaled, scaler) + + # We've changed the frequencies, but not the PSD, so the scaled PBS should be the same as the original as long as we account for the different frequency bin widths via the `scaler`. + @test all(pbs_scaled./scaler .≈ pbs) + # And the band frequencies should all be scaled. + @test all(center_bands(pbs_scaled)./scaler .≈ center_bands(pbs)) + + # Creating a non-lazy version of the PBS should give the same stuff as the lazy version. + pbs_scaled_non_lazy = ProportionalBandSpectrum(typeof(center_bands(pbs_scaled)), center_bands(pbs_scaled)[begin], pbs_scaled, freq_scaler(pbs_scaled)) + @test all(pbs_scaled_non_lazy .≈ pbs_scaled) + @test lower_bands(pbs_scaled_non_lazy) === lower_bands(pbs_scaled) + @test center_bands(pbs_scaled_non_lazy) === center_bands(pbs_scaled) + @test upper_bands(pbs_scaled_non_lazy) === upper_bands(pbs_scaled) + + end + + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(freq_min_nb, df_nb, msp, center_bands(pbs)) + @test all(pbs_init_cbands .≈ pbs) + + # Now, for the tonal stuff, let's make sure we get the right thing, also. + scaler = 1 + tonal = true + pbs_tonal = LazyNBProportionalBandSpectrum(ExactProportionalBands{3}, freq_min_nb, df_nb, msp, scaler, tonal) + + # So, the narrowband frequencies go from 55.0 Hz to 1950 Hz. + # Let's check that. + cbands_tonal = center_bands(pbs_tonal) + @test band_start(cbands_tonal) == 17 + @test band_end(cbands_tonal) == 33 + + # Now check that the pbs is what we expect. + lbands = lower_bands(pbs_tonal) + ubands = upper_bands(pbs_tonal) + for (lband, uband, amp) in zip(lbands, ubands, pbs_tonal) + # First index we want in f_nb is the one that is greater than or equal to lband. + istart = searchsortedfirst(f_nb, lband) + # Last index we want in f_nb is th one that is less than or equal to uband. + iend = searchsortedlast(f_nb, uband) + # Now check that we get the right answer. + @test sum(msp[istart:iend]) ≈ amp + end + + # Now, what about the scaler stuff? + # Should be able to use the same trick for the istonal == false stuff. + for scaler in [0.1, 0.5, 1.0, 1.5, 2.0] + # Now create another PBS, but with the scaled frequency bands, same psd. + freq_min_nb_scaled = 55.0*scaler + freq_max_nb_scaled = 1950.0*scaler + df_nb_scaled = (freq_max_nb_scaled - freq_min_nb_scaled)/(nfreq_nb - 1) + f_nb_scaled = freq_min_nb_scaled .+ (0:(nfreq_nb-1)).*df_nb_scaled + msp_scaled = psd .* df_nb_scaled + pbs_scaled = LazyNBProportionalBandSpectrum(ExactProportionalBands{3}, freq_min_nb_scaled, df_nb_scaled, msp_scaled, scaler, tonal) + + # We've changed the frequencies, but not the PSD, so the scaled PBS should be the same as the original as long as we account for the different frequency bin widths via the `scaler`. + @test all(pbs_scaled./scaler .≈ pbs_tonal) + # And the band frequencies should all be scaled. + @test all(center_bands(pbs_scaled)./scaler .≈ center_bands(pbs_tonal)) + end + + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(freq_min_nb, df_nb, msp, center_bands(pbs_tonal), tonal) + @test all(pbs_init_cbands .≈ pbs_tonal) end # @testset "ANOPP2 docs example" begin @@ -862,8 +1354,8 @@ end # df = psd_freq[2] - psd_freq[1] # msp_amp = 20 .+ 10 .* (1:n_freq)./n_freq # psd_amp = msp_amp ./ df - # # pbs = ExactProportionalBandSpectrum{3}(first(psd_freq), df, psd_amp) - # pbs = ProportionalBandSpectrum(ExactProportionalBands{3}, first(psd_freq), df, psd_amp) + # # pbs = ExactLazyNBProportionalBandSpectrum{3}(first(psd_freq), df, psd_amp) + # pbs = LazyNBProportionalBandSpectrum(ExactProportionalBands{3}, first(psd_freq), df, psd_amp) # cbands = center_bands(pbs) # lbands = lower_bands(pbs) # ubands = upper_bands(pbs) @@ -892,7 +1384,10 @@ end f1 = ubands[b] - 0.5*df_nb f = f0 .+ (0:nfreq-1).*df_nb psd = psd_func.(f) - pbs = ExactThirdOctaveSpectrum(f0, df_nb, psd) + # pbs = LazyNBExactThirdOctaveSpectrum(f0, df_nb, psd) + msp = psd .* df_nb + # pbs = LazyNBExactThirdOctaveSpectrum(f0, df_nb, msp) + pbs = LazyNBProportionalBandSpectrum(ExactProportionalBands{3}, f0, df_nb, msp) if length(pbs) > 1 # We tried above to construct the narrowand frequencies # to only cover the current 1/3-octave proportional @@ -961,8 +1456,11 @@ end psd = psd_func.(f) # And the PBS - # pbs = ExactProportionalBandSpectrum{3}(f[1], df_nb, psd) - pbs = ExactThirdOctaveSpectrum(f[1], df_nb, psd) + # pbs = ExactLazyNBProportionalBandSpectrum{3}(f[1], df_nb, psd) + # pbs = LazyNBExactThirdOctaveSpectrum(f[1], df_nb, psd) + msp = psd .* df_nb + # pbs = LazyNBExactThirdOctaveSpectrum(f[1], df_nb, msp) + pbs = LazyNBProportionalBandSpectrum(ExactProportionalBands{3}, f[1], df_nb, msp) # We created a narrowband range that should cover from freq_min to freq_max, so the sizes should be the same. @test length(pbs) == length(cbands) @@ -996,53 +1494,119 @@ end end @testset "approximate octave" begin - cbands = ApproximateOctaveCenterBands(0, 20) - cbands_expected = [1.0, 2.0, 4.0, 8.0, 16.0, 31.5, 63, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0, 16e3, 31.5e3, 63e3, 125e3, 250e3, 500e3, 1000e3] - @test all(cbands .≈ cbands_expected) + @testset "standard" begin + cbands = ApproximateOctaveCenterBands(0, 20) + cbands_expected = [1.0, 2.0, 4.0, 8.0, 16.0, 31.5, 63, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0, 16e3, 31.5e3, 63e3, 125e3, 250e3, 500e3, 1000e3] + @test all(cbands .≈ cbands_expected) + for (i, cband) in enumerate(cbands) + @test cband_number(cbands, cband) == (band_start(cbands) + i - 1) + end + + lbands = ApproximateOctaveLowerBands(0, 20) + lbands_expected = [0.71, 1.42, 2.84, 5.68, 11.0, 22.0, 44.0, 88.0, 177.0, 355.0, 0.71e3, 1.42e3, 2.84e3, 5.68e3, 11.0e3, 22e3, 44e3, 88e3, 177e3, 355e3, 0.71e6] + @test all(lbands .≈ lbands_expected) + + ubands = ApproximateOctaveUpperBands(0, 20) + ubands_expected = [1.42, 2.84, 5.68, 11.0, 22.0, 44.0, 88.0, 177.0, 355.0, 0.71e3, 1.42e3, 2.84e3, 5.68e3, 11.0e3, 22e3, 44e3, 88e3, 177e3, 355e3, 0.71e6, 1.42e6] + @test all(ubands .≈ ubands_expected) + + cbands = ApproximateOctaveCenterBands(-20, 0) + cbands_expected = [1.0e-6, 2.0e-6, 4.0e-6, 8.0e-6, 16.0e-6, 31.5e-6, 63e-6, 125.0e-6, 250.0e-6, 500.0e-6, 1000.0e-6, 2000.0e-6, 4000.0e-6, 8000.0e-6, 16e-3, 31.5e-3, 63e-3, 125e-3, 250e-3, 500e-3, 1000e-3] + @test all(cbands .≈ cbands_expected) + for (i, cband) in enumerate(cbands) + @test cband_number(cbands, cband) == (band_start(cbands) + i - 1) + end + + lbands = ApproximateOctaveLowerBands(-20, 0) + lbands_expected = [0.71e-6, 1.42e-6, 2.84e-6, 5.68e-6, 11.0e-6, 22.0e-6, 44.0e-6, 88.0e-6, 177.0e-6, 355.0e-6, 0.71e-3, 1.42e-3, 2.84e-3, 5.68e-3, 11.0e-3, 22e-3, 44e-3, 88e-3, 177e-3, 355e-3, 0.71] + @test all(lbands .≈ lbands_expected) + + ubands = ApproximateOctaveUpperBands(-20, 0) + ubands_expected = [1.42e-6, 2.84e-6, 5.68e-6, 11.0e-6, 22.0e-6, 44.0e-6, 88.0e-6, 177.0e-6, 355.0e-6, 0.71e-3, 1.42e-3, 2.84e-3, 5.68e-3, 11.0e-3, 22e-3, 44e-3, 88e-3, 177e-3, 355e-3, 0.71, 1.42] + @test all(ubands .≈ ubands_expected) + + cbands = ApproximateOctaveCenterBands(2.2, 30.5e3) + @test cbands.bstart == 1 + @test cbands.bend == 15 + + lbands = ApproximateOctaveLowerBands(2.2, 30.5e3) + @test lbands.bstart == 1 + @test lbands.bend == 15 + + ubands = ApproximateOctaveUpperBands(2.2, 30.5e3) + @test ubands.bstart == 1 + @test ubands.bend == 15 + + cbands = ApproximateOctaveCenterBands(23.0e-6, 2.8e-3) + @test cbands.bstart == -15 + @test cbands.bend == -9 + + lbands = ApproximateOctaveLowerBands(23.0e-6, 2.8e-3) + @test lbands.bstart == -15 + @test lbands.bend == -9 + + ubands = ApproximateOctaveUpperBands(23.0e-6, 2.8e-3) + @test ubands.bstart == -15 + @test ubands.bend == -9 + end + + @testset "scaler argument" begin + for scaler in [0.1, 0.5, 1.0, 1.5, 2.0] + cbands = ApproximateOctaveCenterBands(0, 20, scaler) + cbands_expected = scaler .* [1.0, 2.0, 4.0, 8.0, 16.0, 31.5, 63, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0, 16e3, 31.5e3, 63e3, 125e3, 250e3, 500e3, 1000e3] + @test all(cbands .≈ cbands_expected) + for (i, cband) in enumerate(cbands) + @test cband_number(cbands, cband) == (band_start(cbands) + i - 1) + end - lbands = ApproximateOctaveLowerBands(0, 20) - lbands_expected = [0.71, 1.42, 2.84, 5.68, 11.0, 22.0, 44.0, 88.0, 177.0, 355.0, 0.71e3, 1.42e3, 2.84e3, 5.68e3, 11.0e3, 22e3, 44e3, 88e3, 177e3, 355e3, 0.71e6] - @test all(lbands .≈ lbands_expected) + lbands = ApproximateOctaveLowerBands(0, 20, scaler) + lbands_expected = scaler .* [0.71, 1.42, 2.84, 5.68, 11.0, 22.0, 44.0, 88.0, 177.0, 355.0, 0.71e3, 1.42e3, 2.84e3, 5.68e3, 11.0e3, 22e3, 44e3, 88e3, 177e3, 355e3, 0.71e6] + @test all(lbands .≈ lbands_expected) - ubands = ApproximateOctaveUpperBands(0, 20) - ubands_expected = [1.42, 2.84, 5.68, 11.0, 22.0, 44.0, 88.0, 177.0, 355.0, 0.71e3, 1.42e3, 2.84e3, 5.68e3, 11.0e3, 22e3, 44e3, 88e3, 177e3, 355e3, 0.71e6, 1.42e6] - @test all(ubands .≈ ubands_expected) + ubands = ApproximateOctaveUpperBands(0, 20, scaler) + ubands_expected = scaler .* [1.42, 2.84, 5.68, 11.0, 22.0, 44.0, 88.0, 177.0, 355.0, 0.71e3, 1.42e3, 2.84e3, 5.68e3, 11.0e3, 22e3, 44e3, 88e3, 177e3, 355e3, 0.71e6, 1.42e6] + @test all(ubands .≈ ubands_expected) - cbands = ApproximateOctaveCenterBands(-20, 0) - cbands_expected = [1.0e-6, 2.0e-6, 4.0e-6, 8.0e-6, 16.0e-6, 31.5e-6, 63e-6, 125.0e-6, 250.0e-6, 500.0e-6, 1000.0e-6, 2000.0e-6, 4000.0e-6, 8000.0e-6, 16e-3, 31.5e-3, 63e-3, 125e-3, 250e-3, 500e-3, 1000e-3] - @test all(cbands .≈ cbands_expected) + cbands = ApproximateOctaveCenterBands(-20, 0, scaler) + cbands_expected = scaler .* [1.0e-6, 2.0e-6, 4.0e-6, 8.0e-6, 16.0e-6, 31.5e-6, 63e-6, 125.0e-6, 250.0e-6, 500.0e-6, 1000.0e-6, 2000.0e-6, 4000.0e-6, 8000.0e-6, 16e-3, 31.5e-3, 63e-3, 125e-3, 250e-3, 500e-3, 1000e-3] + @test all(cbands .≈ cbands_expected) + for (i, cband) in enumerate(cbands) + @test cband_number(cbands, cband) == (band_start(cbands) + i - 1) + end - lbands = ApproximateOctaveLowerBands(-20, 0) - lbands_expected = [0.71e-6, 1.42e-6, 2.84e-6, 5.68e-6, 11.0e-6, 22.0e-6, 44.0e-6, 88.0e-6, 177.0e-6, 355.0e-6, 0.71e-3, 1.42e-3, 2.84e-3, 5.68e-3, 11.0e-3, 22e-3, 44e-3, 88e-3, 177e-3, 355e-3, 0.71] - @test all(lbands .≈ lbands_expected) + lbands = ApproximateOctaveLowerBands(-20, 0, scaler) + lbands_expected = scaler .* [0.71e-6, 1.42e-6, 2.84e-6, 5.68e-6, 11.0e-6, 22.0e-6, 44.0e-6, 88.0e-6, 177.0e-6, 355.0e-6, 0.71e-3, 1.42e-3, 2.84e-3, 5.68e-3, 11.0e-3, 22e-3, 44e-3, 88e-3, 177e-3, 355e-3, 0.71] + @test all(lbands .≈ lbands_expected) - ubands = ApproximateOctaveUpperBands(-20, 0) - ubands_expected = [1.42e-6, 2.84e-6, 5.68e-6, 11.0e-6, 22.0e-6, 44.0e-6, 88.0e-6, 177.0e-6, 355.0e-6, 0.71e-3, 1.42e-3, 2.84e-3, 5.68e-3, 11.0e-3, 22e-3, 44e-3, 88e-3, 177e-3, 355e-3, 0.71, 1.42] - @test all(ubands .≈ ubands_expected) + ubands = ApproximateOctaveUpperBands(-20, 0, scaler) + ubands_expected = scaler .* [1.42e-6, 2.84e-6, 5.68e-6, 11.0e-6, 22.0e-6, 44.0e-6, 88.0e-6, 177.0e-6, 355.0e-6, 0.71e-3, 1.42e-3, 2.84e-3, 5.68e-3, 11.0e-3, 22e-3, 44e-3, 88e-3, 177e-3, 355e-3, 0.71, 1.42] + @test all(ubands .≈ ubands_expected) - cbands = ApproximateOctaveCenterBands(2.2, 30.5e3) - @test cbands.bstart == 1 - @test cbands.bend == 15 + cbands = ApproximateOctaveCenterBands(2.2*scaler, 30.5e3*scaler, scaler) + @test cbands.bstart == 1 + @test cbands.bend == 15 - lbands = ApproximateOctaveLowerBands(2.2, 30.5e3) - @test lbands.bstart == 1 - @test lbands.bend == 15 + lbands = ApproximateOctaveLowerBands(2.2*scaler, 30.5e3*scaler, scaler) + @test lbands.bstart == 1 + @test lbands.bend == 15 - ubands = ApproximateOctaveUpperBands(2.2, 30.5e3) - @test ubands.bstart == 1 - @test ubands.bend == 15 + ubands = ApproximateOctaveUpperBands(2.2*scaler, 30.5e3*scaler, scaler) + @test ubands.bstart == 1 + @test ubands.bend == 15 - cbands = ApproximateOctaveCenterBands(23.0e-6, 2.8e-3) - @test cbands.bstart == -15 - @test cbands.bend == -9 + cbands = ApproximateOctaveCenterBands(23.0e-6*scaler, 2.8e-3*scaler, scaler) + @test cbands.bstart == -15 + @test cbands.bend == -9 - lbands = ApproximateOctaveLowerBands(23.0e-6, 2.8e-3) - @test lbands.bstart == -15 - @test lbands.bend == -9 + lbands = ApproximateOctaveLowerBands(23.0e-6*scaler, 2.8e-3*scaler, scaler) + @test lbands.bstart == -15 + @test lbands.bend == -9 - ubands = ApproximateOctaveUpperBands(23.0e-6, 2.8e-3) - @test ubands.bstart == -15 - @test ubands.bend == -9 + ubands = ApproximateOctaveUpperBands(23.0e-6*scaler, 2.8e-3*scaler, scaler) + @test ubands.bstart == -15 + @test ubands.bend == -9 + end + end @testset "spectrum, normal case" begin freq_min_nb = 55.0 @@ -1050,7 +1614,17 @@ end df_nb = 2.0 f_nb = freq_min_nb:df_nb:freq_max_nb psd = psd_func.(f_nb) - pbs = ProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb, df_nb, psd) + # pbs = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb, df_nb, psd) + msp = psd .* df_nb + pbs = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb, df_nb, msp) + + # Creating a non-lazy version of the PBS should give the same stuff as the lazy version. + pbs_non_lazy = ProportionalBandSpectrum(typeof(center_bands(pbs)), center_bands(pbs)[begin], pbs) + @test all(pbs_non_lazy .≈ pbs) + @test lower_bands(pbs_non_lazy) === lower_bands(pbs) + @test center_bands(pbs_non_lazy) === center_bands(pbs) + @test upper_bands(pbs_non_lazy) === upper_bands(pbs) + lbands = lower_bands(pbs) cbands = center_bands(pbs) ubands = upper_bands(pbs) @@ -1080,6 +1654,74 @@ end res = res_first + sum(psd[istart+1:iend-1].*df_nb) + res_last @test pbs_b ≈ res end + + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(freq_min_nb, df_nb, msp, center_bands(pbs)) + @test all(pbs_init_cbands .≈ pbs) + + # Now, check that the `scaler` argument works. + for scaler in [0.1, 0.5, 1.0, 1.5, 2.0] + freq_min_nb_scaled = freq_min_nb*scaler + freq_max_nb_scaled = freq_max_nb*scaler + df_nb_scaled = df_nb*scaler + # pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb_scaled, df_nb_scaled, psd, scaler) + # pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp, scaler) + msp_scaled = psd .* df_nb_scaled + pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp_scaled, scaler) + + # We've changed the frequencies, but not the PSD, so the scaled PBS should be the original PBS multipiled by `scaler`. + @test all(pbs_scaled./scaler .≈ pbs) + # And the band frequencies should all be scaled. + @test all(lower_bands(pbs_scaled)./scaler .≈ lower_bands(pbs)) + @test all(center_bands(pbs_scaled)./scaler .≈ center_bands(pbs)) + @test all(upper_bands(pbs_scaled)./scaler .≈ upper_bands(pbs)) + + # Creating a non-lazy version of the PBS should give the same stuff as the lazy version. + pbs_scaled_non_lazy = ProportionalBandSpectrum(typeof(center_bands(pbs_scaled)), center_bands(pbs_scaled)[begin], pbs_scaled, freq_scaler(pbs_scaled)) + @test all(pbs_scaled_non_lazy .≈ pbs_scaled) + @test lower_bands(pbs_scaled_non_lazy) === lower_bands(pbs_scaled) + @test center_bands(pbs_scaled_non_lazy) === center_bands(pbs_scaled) + @test upper_bands(pbs_scaled_non_lazy) === upper_bands(pbs_scaled) + end + + # Now, for the tonal stuff. + scaler = 1 + tonal = true + pbs_tonal = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb, df_nb, msp, scaler, tonal) + # Narrowband frequencies go from 55 Hz to 1950 Hz, so check that. + cbands = center_bands(pbs_tonal) + @test band_start(cbands) == 6 + @test band_end(cbands) == 11 + + # Now make sure we get the right answer. + lbands = lower_bands(pbs_tonal) + ubands = upper_bands(pbs_tonal) + for (lband, uband, amp) in zip(lbands, ubands, pbs_tonal) + # First index we want in f_nb is the one that is greater than or equal to lband. + istart = searchsortedfirst(f_nb, lband) + # Last index we want in f_nb is th one that is less than or equal to uband. + iend = searchsortedlast(f_nb, uband; lt=<=) + # Now check that we get the right answer. + @test sum(msp[istart:iend]) ≈ amp + end + + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(freq_min_nb, df_nb, msp, center_bands(pbs_tonal), tonal) + @test all(pbs_init_cbands .≈ pbs_tonal) + + # Now for the scaler stuff, can use the same trick for the non-tonal. + for scaler in [0.1, 0.5, 1.0, 1.5, 2.0] + freq_min_nb_scaled = freq_min_nb*scaler + freq_max_nb_scaled = freq_max_nb*scaler + df_nb_scaled = df_nb*scaler + msp_scaled = psd .* df_nb_scaled + pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp_scaled, scaler, tonal) + + # We've changed the frequencies, but not the PSD, so the scaled PBS should be the same as the original as long as we account for the different frequency bin widths via the `scaler`. + @test all(pbs_scaled./scaler .≈ pbs_tonal) + # And the band frequencies should all be scaled. + @test all(center_bands(pbs_scaled)./scaler .≈ center_bands(pbs_tonal)) + end end @testset "spectrum, lowest narrowband on a right edge" begin @@ -1088,7 +1730,17 @@ end df_nb = 2.0 f_nb = freq_min_nb:df_nb:freq_max_nb psd = psd_func.(f_nb) - pbs = ProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb, df_nb, psd) + # pbs = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb, df_nb, psd) + msp = psd .* df_nb + pbs = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb, df_nb, msp) + + # Creating a non-lazy version of the PBS should give the same stuff as the lazy version. + pbs_non_lazy = ProportionalBandSpectrum(typeof(center_bands(pbs)), center_bands(pbs)[begin], pbs) + @test all(pbs_non_lazy .≈ pbs) + @test lower_bands(pbs_non_lazy) === lower_bands(pbs) + @test center_bands(pbs_non_lazy) === center_bands(pbs) + @test upper_bands(pbs_non_lazy) === upper_bands(pbs) + lbands = lower_bands(pbs) cbands = center_bands(pbs) ubands = upper_bands(pbs) @@ -1112,6 +1764,76 @@ end res = res_first + sum(psd[istart+1:iend-1].*df_nb) + res_last @test pbs_b ≈ res end + + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(freq_min_nb, df_nb, msp, center_bands(pbs)) + @test all(pbs_init_cbands .≈ pbs) + + # Now, check that the `scaler` argument works. + for scaler in [0.1, 0.5, 1.0, 1.5, 2.0] + freq_min_nb_scaled = freq_min_nb*scaler + freq_max_nb_scaled = freq_max_nb*scaler + df_nb_scaled = df_nb*scaler + # pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb_scaled, df_nb_scaled, psd, scaler) + # pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp, scaler) + msp_scaled = psd .* df_nb_scaled + pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp_scaled, scaler) + + # We've changed the frequencies, but not the PSD, so the scaled PBS should be the original PBS multipiled by `scaler`. + @test all(pbs_scaled./scaler .≈ pbs) + # And the band frequencies should all be scaled. + @test all(lower_bands(pbs_scaled)./scaler .≈ lower_bands(pbs)) + @test all(center_bands(pbs_scaled)./scaler .≈ center_bands(pbs)) + @test all(upper_bands(pbs_scaled)./scaler .≈ upper_bands(pbs)) + + # Creating a non-lazy version of the PBS should give the same stuff as the lazy version. + pbs_scaled_non_lazy = ProportionalBandSpectrum(typeof(center_bands(pbs_scaled)), center_bands(pbs_scaled)[begin], pbs_scaled, freq_scaler(pbs_scaled)) + @test all(pbs_scaled_non_lazy .≈ pbs_scaled) + @test lower_bands(pbs_scaled_non_lazy) === lower_bands(pbs_scaled) + @test center_bands(pbs_scaled_non_lazy) === center_bands(pbs_scaled) + @test upper_bands(pbs_scaled_non_lazy) === upper_bands(pbs_scaled) + end + + # Now, for the tonal stuff. + scaler = 1 + tonal = true + pbs_tonal = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb, df_nb, msp, scaler, tonal) + # Narrowband frequencies go from 87 Hz to 1950 Hz, so check that. + cbands = center_bands(pbs_tonal) + @test band_start(cbands) == 6 + @test band_end(cbands) == 11 + + # Now make sure we get the right answer. + lbands = lower_bands(pbs_tonal) + ubands = upper_bands(pbs_tonal) + for (lband, uband, amp) in zip(lbands, ubands, pbs_tonal) + # First index we want in f_nb is the one that is greater than or equal to lband. + istart = searchsortedfirst(f_nb, lband) + # Last index we want in f_nb is th one that is less than or equal to uband. + iend = searchsortedlast(f_nb, uband; lt=<=) + # Now check that we get the right answer. + @test sum(msp[istart:iend]) ≈ amp + end + + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(freq_min_nb, df_nb, msp, center_bands(pbs_tonal), tonal) + @test all(pbs_init_cbands .≈ pbs_tonal) + + # Now for the scaler stuff, can use the same trick for the non-tonal. + for scaler in [0.1, 0.5, 1.0, 1.5, 2.0] + freq_min_nb_scaled = freq_min_nb*scaler + freq_max_nb_scaled = freq_max_nb*scaler + df_nb_scaled = df_nb*scaler + msp_scaled = psd .* df_nb_scaled + pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp_scaled, scaler, tonal) + + # We've changed the frequencies, but not the PSD, so the scaled PBS should be the same as the original as long as we account for the different frequency bin widths via the `scaler`. + @test all(pbs_scaled./scaler .≈ pbs_tonal) + # And the band frequencies should all be scaled. + @test all(lower_bands(pbs_scaled)./scaler .≈ lower_bands(pbs_tonal)) + @test all(center_bands(pbs_scaled)./scaler .≈ center_bands(pbs_tonal)) + @test all(upper_bands(pbs_scaled)./scaler .≈ upper_bands(pbs_tonal)) + end end @testset "spectrum, lowest narrowband on a left edge" begin @@ -1120,7 +1842,17 @@ end df_nb = 2.0 f_nb = freq_min_nb:df_nb:freq_max_nb psd = psd_func.(f_nb) - pbs = ProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb, df_nb, psd) + # pbs = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb, df_nb, psd) + msp = psd .* df_nb + pbs = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb, df_nb, msp) + + # Creating a non-lazy version of the PBS should give the same stuff as the lazy version. + pbs_non_lazy = ProportionalBandSpectrum(typeof(center_bands(pbs)), center_bands(pbs)[begin], pbs) + @test all(pbs_non_lazy .≈ pbs) + @test lower_bands(pbs_non_lazy) === lower_bands(pbs) + @test center_bands(pbs_non_lazy) === center_bands(pbs) + @test upper_bands(pbs_non_lazy) === upper_bands(pbs) + lbands = lower_bands(pbs) cbands = center_bands(pbs) ubands = upper_bands(pbs) @@ -1145,6 +1877,76 @@ end res = res_first + sum(psd[istart+1:iend-1].*df_nb) + res_last @test pbs_b ≈ res end + + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(freq_min_nb, df_nb, msp, center_bands(pbs)) + @test all(pbs_init_cbands .≈ pbs) + + # Now, check that the `scaler` argument works. + for scaler in [0.1, 0.5, 1.0, 1.5, 2.0] + freq_min_nb_scaled = freq_min_nb*scaler + freq_max_nb_scaled = freq_max_nb*scaler + df_nb_scaled = df_nb*scaler + # pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb_scaled, df_nb_scaled, psd, scaler) + # pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp, scaler) + msp_scaled = psd .* df_nb_scaled + pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp_scaled, scaler) + + # We've changed the frequencies, but not the PSD, so the scaled PBS should be the original PBS multipiled by `scaler`. + @test all(pbs_scaled./scaler .≈ pbs) + # And the band frequencies should all be scaled. + @test all(lower_bands(pbs_scaled)./scaler .≈ lower_bands(pbs)) + @test all(center_bands(pbs_scaled)./scaler .≈ center_bands(pbs)) + @test all(upper_bands(pbs_scaled)./scaler .≈ upper_bands(pbs)) + + # Creating a non-lazy version of the PBS should give the same stuff as the lazy version. + pbs_scaled_non_lazy = ProportionalBandSpectrum(typeof(center_bands(pbs_scaled)), center_bands(pbs_scaled)[begin], pbs_scaled, freq_scaler(pbs_scaled)) + @test all(pbs_scaled_non_lazy .≈ pbs_scaled) + @test lower_bands(pbs_scaled_non_lazy) === lower_bands(pbs_scaled) + @test center_bands(pbs_scaled_non_lazy) === center_bands(pbs_scaled) + @test upper_bands(pbs_scaled_non_lazy) === upper_bands(pbs_scaled) + end + + # Now, for the tonal stuff. + scaler = 1 + tonal = true + pbs_tonal = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb, df_nb, msp, scaler, tonal) + # Narrowband frequencies go from 89 Hz to 1950 Hz, so check that. + cbands = center_bands(pbs_tonal) + @test band_start(cbands) == 7 + @test band_end(cbands) == 11 + + # Now make sure we get the right answer. + lbands = lower_bands(pbs_tonal) + ubands = upper_bands(pbs_tonal) + for (lband, uband, amp) in zip(lbands, ubands, pbs_tonal) + # First index we want in f_nb is the one that is greater than or equal to lband. + istart = searchsortedfirst(f_nb, lband) + # Last index we want in f_nb is th one that is less than or equal to uband. + iend = searchsortedlast(f_nb, uband; lt=<=) + # Now check that we get the right answer. + @test sum(msp[istart:iend]) ≈ amp + end + + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(freq_min_nb, df_nb, msp, center_bands(pbs_tonal), tonal) + @test all(pbs_init_cbands .≈ pbs_tonal) + + # Now for the scaler stuff, can use the same trick for the non-tonal. + for scaler in [0.1, 0.5, 1.0, 1.5, 2.0] + freq_min_nb_scaled = freq_min_nb*scaler + freq_max_nb_scaled = freq_max_nb*scaler + df_nb_scaled = df_nb*scaler + msp_scaled = psd .* df_nb_scaled + pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp_scaled, scaler, tonal) + + # We've changed the frequencies, but not the PSD, so the scaled PBS should be the same as the original as long as we account for the different frequency bin widths via the `scaler`. + @test all(pbs_scaled./scaler .≈ pbs_tonal) + # And the band frequencies should all be scaled. + @test all(lower_bands(pbs_scaled)./scaler .≈ lower_bands(pbs_tonal)) + @test all(center_bands(pbs_scaled)./scaler .≈ center_bands(pbs_tonal)) + @test all(upper_bands(pbs_scaled)./scaler .≈ upper_bands(pbs_tonal)) + end end @testset "spectrum, highest narrowband on a left edge" begin @@ -1153,7 +1955,17 @@ end df_nb = 2.0 f_nb = freq_min_nb:df_nb:freq_max_nb psd = psd_func.(f_nb) - pbs = ProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb, df_nb, psd) + # pbs = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb, df_nb, psd) + msp = psd .* df_nb + pbs = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb, df_nb, msp) + + # Creating a non-lazy version of the PBS should give the same stuff as the lazy version. + pbs_non_lazy = ProportionalBandSpectrum(typeof(center_bands(pbs)), center_bands(pbs)[begin], pbs) + @test all(pbs_non_lazy .≈ pbs) + @test lower_bands(pbs_non_lazy) === lower_bands(pbs) + @test center_bands(pbs_non_lazy) === center_bands(pbs) + @test upper_bands(pbs_non_lazy) === upper_bands(pbs) + lbands = lower_bands(pbs) cbands = center_bands(pbs) ubands = upper_bands(pbs) @@ -1184,6 +1996,76 @@ end res = res_first + sum(psd[istart+1:iend-1].*df_nb) + res_last @test pbs_b ≈ res end + + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(freq_min_nb, df_nb, msp, center_bands(pbs)) + @test all(pbs_init_cbands .≈ pbs) + + # Now, check that the `scaler` argument works. + for scaler in [0.1, 0.5, 1.0, 1.5, 2.0] + freq_min_nb_scaled = freq_min_nb*scaler + freq_max_nb_scaled = freq_max_nb*scaler + df_nb_scaled = df_nb*scaler + # pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb_scaled, df_nb_scaled, psd, scaler) + # pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp, scaler) + msp_scaled = psd .* df_nb_scaled + pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp_scaled, scaler) + + # We've changed the frequencies, but not the PSD, so the scaled PBS should be the original PBS multipiled by `scaler`. + @test all(pbs_scaled./scaler .≈ pbs) + # And the band frequencies should all be scaled. + @test all(lower_bands(pbs_scaled)./scaler .≈ lower_bands(pbs)) + @test all(center_bands(pbs_scaled)./scaler .≈ center_bands(pbs)) + @test all(upper_bands(pbs_scaled)./scaler .≈ upper_bands(pbs)) + + # Creating a non-lazy version of the PBS should give the same stuff as the lazy version. + pbs_scaled_non_lazy = ProportionalBandSpectrum(typeof(center_bands(pbs_scaled)), center_bands(pbs_scaled)[begin], pbs_scaled, freq_scaler(pbs_scaled)) + @test all(pbs_scaled_non_lazy .≈ pbs_scaled) + @test lower_bands(pbs_scaled_non_lazy) === lower_bands(pbs_scaled) + @test center_bands(pbs_scaled_non_lazy) === center_bands(pbs_scaled) + @test upper_bands(pbs_scaled_non_lazy) === upper_bands(pbs_scaled) + end + + # Now, for the tonal stuff. + scaler = 1 + tonal = true + pbs_tonal = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb, df_nb, msp, scaler, tonal) + # Narrowband frequencies go from 55 Hz to 1421 Hz, so check that. + cbands = center_bands(pbs_tonal) + @test band_start(cbands) == 6 + @test band_end(cbands) == 11 + + # Now make sure we get the right answer. + lbands = lower_bands(pbs_tonal) + ubands = upper_bands(pbs_tonal) + for (lband, uband, amp) in zip(lbands, ubands, pbs_tonal) + # First index we want in f_nb is the one that is greater than or equal to lband. + istart = searchsortedfirst(f_nb, lband) + # Last index we want in f_nb is th one that is less than or equal to uband. + iend = searchsortedlast(f_nb, uband; lt=<=) + # Now check that we get the right answer. + @test sum(msp[istart:iend]) ≈ amp + end + + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(freq_min_nb, df_nb, msp, center_bands(pbs_tonal), tonal) + @test all(pbs_init_cbands .≈ pbs_tonal) + + # Now for the scaler stuff, can use the same trick for the non-tonal. + for scaler in [0.1, 0.5, 1.0, 1.5, 2.0] + freq_min_nb_scaled = freq_min_nb*scaler + freq_max_nb_scaled = freq_max_nb*scaler + df_nb_scaled = df_nb*scaler + msp_scaled = psd .* df_nb_scaled + pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp_scaled, scaler, tonal) + + # We've changed the frequencies, but not the PSD, so the scaled PBS should be the same as the original as long as we account for the different frequency bin widths via the `scaler`. + @test all(pbs_scaled./scaler .≈ pbs_tonal) + # And the band frequencies should all be scaled. + @test all(lower_bands(pbs_scaled)./scaler .≈ lower_bands(pbs_tonal)) + @test all(center_bands(pbs_scaled)./scaler .≈ center_bands(pbs_tonal)) + @test all(upper_bands(pbs_scaled)./scaler .≈ upper_bands(pbs_tonal)) + end end @testset "spectrum, highest narrowband on a right edge" begin @@ -1192,7 +2074,17 @@ end df_nb = 2.0 f_nb = freq_min_nb:df_nb:freq_max_nb psd = psd_func.(f_nb) - pbs = ProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb, df_nb, psd) + # pbs = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb, df_nb, psd) + msp = psd .* df_nb + pbs = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb, df_nb, msp) + + # Creating a non-lazy version of the PBS should give the same stuff as the lazy version. + pbs_non_lazy = ProportionalBandSpectrum(typeof(center_bands(pbs)), center_bands(pbs)[begin], pbs) + @test all(pbs_non_lazy .≈ pbs) + @test lower_bands(pbs_non_lazy) === lower_bands(pbs) + @test center_bands(pbs_non_lazy) === center_bands(pbs) + @test upper_bands(pbs_non_lazy) === upper_bands(pbs) + lbands = lower_bands(pbs) cbands = center_bands(pbs) ubands = upper_bands(pbs) @@ -1223,41 +2115,171 @@ end res = res_first + sum(psd[istart+1:iend-1].*df_nb) + res_last @test pbs_b ≈ res end + + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(freq_min_nb, df_nb, msp, center_bands(pbs)) + @test all(pbs_init_cbands .≈ pbs) + + # Now, check that the `scaler` argument works. + for scaler in [0.1, 0.5, 1.0, 1.5, 2.0] + freq_min_nb_scaled = freq_min_nb*scaler + freq_max_nb_scaled = freq_max_nb*scaler + df_nb_scaled = df_nb*scaler + # pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb_scaled, df_nb_scaled, psd, scaler) + # pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp, scaler) + msp_scaled = psd .* df_nb_scaled + pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp_scaled, scaler) + + # We've changed the frequencies, but not the PSD, so the scaled PBS should be the original PBS multipiled by `scaler`. + @test all(pbs_scaled./scaler .≈ pbs) + # And the band frequencies should all be scaled. + @test all(lower_bands(pbs_scaled)./scaler .≈ lower_bands(pbs)) + @test all(center_bands(pbs_scaled)./scaler .≈ center_bands(pbs)) + @test all(upper_bands(pbs_scaled)./scaler .≈ upper_bands(pbs)) + + # Creating a non-lazy version of the PBS should give the same stuff as the lazy version. + pbs_scaled_non_lazy = ProportionalBandSpectrum(typeof(center_bands(pbs_scaled)), center_bands(pbs_scaled)[begin], pbs_scaled, freq_scaler(pbs_scaled)) + @test all(pbs_scaled_non_lazy .≈ pbs_scaled) + @test lower_bands(pbs_scaled_non_lazy) === lower_bands(pbs_scaled) + @test center_bands(pbs_scaled_non_lazy) === center_bands(pbs_scaled) + @test upper_bands(pbs_scaled_non_lazy) === upper_bands(pbs_scaled) + end + + # Now, for the tonal stuff. + scaler = 1 + tonal = true + pbs_tonal = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb, df_nb, msp, scaler, tonal) + # Narrowband frequencies go from 55 Hz to 1419 Hz, so check that. + cbands = center_bands(pbs_tonal) + @test band_start(cbands) == 6 + @test band_end(cbands) == 10 + + # Now make sure we get the right answer. + lbands = lower_bands(pbs_tonal) + ubands = upper_bands(pbs_tonal) + for (lband, uband, amp) in zip(lbands, ubands, pbs_tonal) + # First index we want in f_nb is the one that is greater than or equal to lband. + istart = searchsortedfirst(f_nb, lband) + # Last index we want in f_nb is th one that is less than or equal to uband. + iend = searchsortedlast(f_nb, uband; lt=<=) + # Now check that we get the right answer. + @test sum(msp[istart:iend]) ≈ amp + end + + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(freq_min_nb, df_nb, msp, center_bands(pbs_tonal), tonal) + @test all(pbs_init_cbands .≈ pbs_tonal) + + # Now for the scaler stuff, can use the same trick for the non-tonal. + for scaler in [0.1, 0.5, 1.0, 1.5, 2.0] + freq_min_nb_scaled = freq_min_nb*scaler + freq_max_nb_scaled = freq_max_nb*scaler + df_nb_scaled = df_nb*scaler + msp_scaled = psd .* df_nb_scaled + pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp_scaled, scaler, tonal) + + # We've changed the frequencies, but not the PSD, so the scaled PBS should be the same as the original as long as we account for the different frequency bin widths via the `scaler`. + @test all(pbs_scaled./scaler .≈ pbs_tonal) + # And the band frequencies should all be scaled. + @test all(lower_bands(pbs_scaled)./scaler .≈ lower_bands(pbs_tonal)) + @test all(center_bands(pbs_scaled)./scaler .≈ center_bands(pbs_tonal)) + @test all(upper_bands(pbs_scaled)./scaler .≈ upper_bands(pbs_tonal)) + end end end @testset "approximate 1/3rd octave" begin - cbands = ApproximateThirdOctaveCenterBands(0, 30) - cbands_expected = [1.0, 1.25, 1.6, 2.0, 2.5, 3.15, 4.0, 5.0, 6.3, 8.0, 1.0e1, 1.25e1, 1.6e1, 2.0e1, 2.5e1, 3.15e1, 4.0e1, 5.0e1, 6.3e1, 8.0e1, 1.0e2, 1.25e2, 1.6e2, 2.0e2, 2.5e2, 3.15e2, 4.0e2, 5.0e2, 6.3e2, 8.0e2, 1.0e3] - @test all(cbands .≈ cbands_expected) + @testset "standard" begin + cbands = ApproximateThirdOctaveCenterBands(0, 30) + cbands_expected = [1.0, 1.25, 1.6, 2.0, 2.5, 3.15, 4.0, 5.0, 6.3, 8.0, 1.0e1, 1.25e1, 1.6e1, 2.0e1, 2.5e1, 3.15e1, 4.0e1, 5.0e1, 6.3e1, 8.0e1, 1.0e2, 1.25e2, 1.6e2, 2.0e2, 2.5e2, 3.15e2, 4.0e2, 5.0e2, 6.3e2, 8.0e2, 1.0e3] + @test all(cbands .≈ cbands_expected) + + # Test the `cband_number` routine, which goes from an approximate 3rd-octave centerband frequency to the band number. + for (i, cband) in enumerate(cbands) + @test cband_number(cbands, cband) == (band_start(cbands) + i - 1) + end + + lbands = ApproximateThirdOctaveLowerBands(0, 30) + lbands_expected = [0.9, 1.12, 1.4, 1.8, 2.24, 2.8, 3.35, 4.5, 5.6, 7.1, 0.9e1, 1.12e1, 1.4e1, 1.8e1, 2.24e1, 2.8e1, 3.35e1, 4.5e1, 5.6e1, 7.1e1, 0.9e2, 1.12e2, 1.4e2, 1.8e2, 2.24e2, 2.8e2, 3.35e2, 4.5e2, 5.6e2, 7.1e2, 0.9e3] + @test all(lbands .≈ lbands_expected) + + ubands = ApproximateThirdOctaveUpperBands(0, 30) + ubands_expected = [1.12, 1.4, 1.8, 2.24, 2.8, 3.35, 4.5, 5.6, 7.1, 0.9e1, 1.12e1, 1.4e1, 1.8e1, 2.24e1, 2.8e1, 3.35e1, 4.5e1, 5.6e1, 7.1e1, 0.9e2, 1.12e2, 1.4e2, 1.8e2, 2.24e2, 2.8e2, 3.35e2, 4.5e2, 5.6e2, 7.1e2, 0.9e3, 1.12e3] + @test all(ubands .≈ ubands_expected) + + cbands = ApproximateThirdOctaveCenterBands(-30, 0) + cbands_expected = [1.0e-3, 1.25e-3, 1.6e-3, 2.0e-3, 2.5e-3, 3.15e-3, 4.0e-3, 5.0e-3, 6.3e-3, 8.0e-3, 1.0e-2, 1.25e-2, 1.6e-2, 2.0e-2, 2.5e-2, 3.15e-2, 4.0e-2, 5.0e-2, 6.3e-2, 8.0e-2, 1.0e-1, 1.25e-1, 1.6e-1, 2.0e-1, 2.5e-1, 3.15e-1, 4.0e-1, 5.0e-1, 6.3e-1, 8.0e-1, 1.0] + @test all(cbands .≈ cbands_expected) + + for (i, cband) in enumerate(cbands) + @test cband_number(cbands, cband) == (band_start(cbands) + i - 1) + end + + lbands = ApproximateThirdOctaveLowerBands(-30, 0) + lbands_expected = [0.9e-3, 1.12e-3, 1.4e-3, 1.8e-3, 2.24e-3, 2.8e-3, 3.35e-3, 4.5e-3, 5.6e-3, 7.1e-3, 0.9e-2, 1.12e-2, 1.4e-2, 1.8e-2, 2.24e-2, 2.8e-2, 3.35e-2, 4.5e-2, 5.6e-2, 7.1e-2, 0.9e-1, 1.12e-1, 1.4e-1, 1.8e-1, 2.24e-1, 2.8e-1, 3.35e-1, 4.5e-1, 5.6e-1, 7.1e-1, 0.9] + @test all(lbands .≈ lbands_expected) + + ubands = ApproximateThirdOctaveUpperBands(-30, 0) + ubands_expected = [1.12e-3, 1.4e-3, 1.8e-3, 2.24e-3, 2.8e-3, 3.35e-3, 4.5e-3, 5.6e-3, 7.1e-3, 0.9e-2, 1.12e-2, 1.4e-2, 1.8e-2, 2.24e-2, 2.8e-2, 3.35e-2, 4.5e-2, 5.6e-2, 7.1e-2, 0.9e-1, 1.12e-1, 1.4e-1, 1.8e-1, 2.24e-1, 2.8e-1, 3.35e-1, 4.5e-1, 5.6e-1, 7.1e-1, 0.9, 1.12] + @test all(ubands .≈ ubands_expected) + end + + @testset "scaler argument" begin + for scaler in [0.1, 0.5, 1.0, 1.5, 2.0] + cbands = ApproximateThirdOctaveCenterBands(0, 30, scaler) + cbands_expected = scaler .* [1.0, 1.25, 1.6, 2.0, 2.5, 3.15, 4.0, 5.0, 6.3, 8.0, 1.0e1, 1.25e1, 1.6e1, 2.0e1, 2.5e1, 3.15e1, 4.0e1, 5.0e1, 6.3e1, 8.0e1, 1.0e2, 1.25e2, 1.6e2, 2.0e2, 2.5e2, 3.15e2, 4.0e2, 5.0e2, 6.3e2, 8.0e2, 1.0e3] + @test all(cbands .≈ cbands_expected) + + # Test the `cband_number` routine, which goes from an approximate 3rd-octave centerband frequency to the band number. + for (i, cband) in enumerate(cbands) + @test cband_number(cbands, cband) == (band_start(cbands) + i - 1) + end + + lbands = ApproximateThirdOctaveLowerBands(0, 30, scaler) + lbands_expected = scaler .* [0.9, 1.12, 1.4, 1.8, 2.24, 2.8, 3.35, 4.5, 5.6, 7.1, 0.9e1, 1.12e1, 1.4e1, 1.8e1, 2.24e1, 2.8e1, 3.35e1, 4.5e1, 5.6e1, 7.1e1, 0.9e2, 1.12e2, 1.4e2, 1.8e2, 2.24e2, 2.8e2, 3.35e2, 4.5e2, 5.6e2, 7.1e2, 0.9e3] + @test all(lbands .≈ lbands_expected) - lbands = ApproximateThirdOctaveLowerBands(0, 30) - lbands_expected = [0.9, 1.12, 1.4, 1.8, 2.24, 2.8, 3.35, 4.5, 5.6, 7.1, 0.9e1, 1.12e1, 1.4e1, 1.8e1, 2.24e1, 2.8e1, 3.35e1, 4.5e1, 5.6e1, 7.1e1, 0.9e2, 1.12e2, 1.4e2, 1.8e2, 2.24e2, 2.8e2, 3.35e2, 4.5e2, 5.6e2, 7.1e2, 0.9e3] - @test all(lbands .≈ lbands_expected) + ubands = ApproximateThirdOctaveUpperBands(0, 30, scaler) + ubands_expected = scaler .* [1.12, 1.4, 1.8, 2.24, 2.8, 3.35, 4.5, 5.6, 7.1, 0.9e1, 1.12e1, 1.4e1, 1.8e1, 2.24e1, 2.8e1, 3.35e1, 4.5e1, 5.6e1, 7.1e1, 0.9e2, 1.12e2, 1.4e2, 1.8e2, 2.24e2, 2.8e2, 3.35e2, 4.5e2, 5.6e2, 7.1e2, 0.9e3, 1.12e3] + @test all(ubands .≈ ubands_expected) - ubands = ApproximateThirdOctaveUpperBands(0, 30) - ubands_expected = [1.12, 1.4, 1.8, 2.24, 2.8, 3.35, 4.5, 5.6, 7.1, 0.9e1, 1.12e1, 1.4e1, 1.8e1, 2.24e1, 2.8e1, 3.35e1, 4.5e1, 5.6e1, 7.1e1, 0.9e2, 1.12e2, 1.4e2, 1.8e2, 2.24e2, 2.8e2, 3.35e2, 4.5e2, 5.6e2, 7.1e2, 0.9e3, 1.12e3] - @test all(ubands .≈ ubands_expected) + cbands = ApproximateThirdOctaveCenterBands(-30, 0, scaler) + cbands_expected = scaler .* [1.0e-3, 1.25e-3, 1.6e-3, 2.0e-3, 2.5e-3, 3.15e-3, 4.0e-3, 5.0e-3, 6.3e-3, 8.0e-3, 1.0e-2, 1.25e-2, 1.6e-2, 2.0e-2, 2.5e-2, 3.15e-2, 4.0e-2, 5.0e-2, 6.3e-2, 8.0e-2, 1.0e-1, 1.25e-1, 1.6e-1, 2.0e-1, 2.5e-1, 3.15e-1, 4.0e-1, 5.0e-1, 6.3e-1, 8.0e-1, 1.0] + @test all(cbands .≈ cbands_expected) - cbands = ApproximateThirdOctaveCenterBands(-30, 0) - cbands_expected = [1.0e-3, 1.25e-3, 1.6e-3, 2.0e-3, 2.5e-3, 3.15e-3, 4.0e-3, 5.0e-3, 6.3e-3, 8.0e-3, 1.0e-2, 1.25e-2, 1.6e-2, 2.0e-2, 2.5e-2, 3.15e-2, 4.0e-2, 5.0e-2, 6.3e-2, 8.0e-2, 1.0e-1, 1.25e-1, 1.6e-1, 2.0e-1, 2.5e-1, 3.15e-1, 4.0e-1, 5.0e-1, 6.3e-1, 8.0e-1, 1.0] - @test all(cbands .≈ cbands_expected) + for (i, cband) in enumerate(cbands) + @test cband_number(cbands, cband) == (band_start(cbands) + i - 1) + end - lbands = ApproximateThirdOctaveLowerBands(-30, 0) - lbands_expected = [0.9e-3, 1.12e-3, 1.4e-3, 1.8e-3, 2.24e-3, 2.8e-3, 3.35e-3, 4.5e-3, 5.6e-3, 7.1e-3, 0.9e-2, 1.12e-2, 1.4e-2, 1.8e-2, 2.24e-2, 2.8e-2, 3.35e-2, 4.5e-2, 5.6e-2, 7.1e-2, 0.9e-1, 1.12e-1, 1.4e-1, 1.8e-1, 2.24e-1, 2.8e-1, 3.35e-1, 4.5e-1, 5.6e-1, 7.1e-1, 0.9] - @test all(lbands .≈ lbands_expected) + lbands = ApproximateThirdOctaveLowerBands(-30, 0, scaler) + lbands_expected = scaler .* [0.9e-3, 1.12e-3, 1.4e-3, 1.8e-3, 2.24e-3, 2.8e-3, 3.35e-3, 4.5e-3, 5.6e-3, 7.1e-3, 0.9e-2, 1.12e-2, 1.4e-2, 1.8e-2, 2.24e-2, 2.8e-2, 3.35e-2, 4.5e-2, 5.6e-2, 7.1e-2, 0.9e-1, 1.12e-1, 1.4e-1, 1.8e-1, 2.24e-1, 2.8e-1, 3.35e-1, 4.5e-1, 5.6e-1, 7.1e-1, 0.9] + @test all(lbands .≈ lbands_expected) - ubands = ApproximateThirdOctaveUpperBands(-30, 0) - ubands_expected = [1.12e-3, 1.4e-3, 1.8e-3, 2.24e-3, 2.8e-3, 3.35e-3, 4.5e-3, 5.6e-3, 7.1e-3, 0.9e-2, 1.12e-2, 1.4e-2, 1.8e-2, 2.24e-2, 2.8e-2, 3.35e-2, 4.5e-2, 5.6e-2, 7.1e-2, 0.9e-1, 1.12e-1, 1.4e-1, 1.8e-1, 2.24e-1, 2.8e-1, 3.35e-1, 4.5e-1, 5.6e-1, 7.1e-1, 0.9, 1.12] - @test all(ubands .≈ ubands_expected) + ubands = ApproximateThirdOctaveUpperBands(-30, 0, scaler) + ubands_expected = scaler .* [1.12e-3, 1.4e-3, 1.8e-3, 2.24e-3, 2.8e-3, 3.35e-3, 4.5e-3, 5.6e-3, 7.1e-3, 0.9e-2, 1.12e-2, 1.4e-2, 1.8e-2, 2.24e-2, 2.8e-2, 3.35e-2, 4.5e-2, 5.6e-2, 7.1e-2, 0.9e-1, 1.12e-1, 1.4e-1, 1.8e-1, 2.24e-1, 2.8e-1, 3.35e-1, 4.5e-1, 5.6e-1, 7.1e-1, 0.9, 1.12] + @test all(ubands .≈ ubands_expected) + end + end @testset "spectrum, normal case" begin freq_min_nb = 50.0 - freq_max_nb = 1950.0 + # freq_max_nb = 1950.0 + nfreq = 951 df_nb = 2.0 - f_nb = freq_min_nb:df_nb:freq_max_nb + # f_nb = freq_min_nb:df_nb:freq_max_nb + f_nb = freq_min_nb .+ (0:nfreq-1) .* df_nb psd = psd_func.(f_nb) - pbs = ProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb, df_nb, psd) + # pbs = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb, df_nb, psd) + msp = psd .* df_nb + pbs = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb, df_nb, msp) + + # Creating a non-lazy version of the PBS should give the same stuff as the lazy version. + pbs_non_lazy = ProportionalBandSpectrum(typeof(center_bands(pbs)), center_bands(pbs)[begin], pbs) + @test all(pbs_non_lazy .≈ pbs) + @test lower_bands(pbs_non_lazy) === lower_bands(pbs) + @test center_bands(pbs_non_lazy) === center_bands(pbs) + @test upper_bands(pbs_non_lazy) === upper_bands(pbs) + lbands = lower_bands(pbs) cbands = center_bands(pbs) ubands = upper_bands(pbs) @@ -1280,15 +2302,102 @@ end res = res_first + sum(psd[istart+1:iend-1].*df_nb) + res_last @test pbs_b ≈ res end - end - @testset "spectrum, lowest narrowband on a right edge" begin - freq_min_nb = 55.0 - freq_max_nb = 1950.0 + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(freq_min_nb, df_nb, msp, center_bands(pbs)) + @test all(pbs_init_cbands .≈ pbs) + + # Now, check that the `scaler` argument works. + for scaler in [0.1, 0.5, 1.0, 1.5, 2.0] + freq_min_nb_scaled = freq_min_nb*scaler + # freq_max_nb_scaled = freq_max_nb*scaler + df_nb_scaled = df_nb*scaler + # pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb_scaled, df_nb_scaled, psd, scaler) + # pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp, scaler) + msp_scaled = psd .* df_nb_scaled + pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp_scaled, scaler) + + # We've changed the frequencies, but not the PSD, so the scaled PBS should be the original PBS multipiled by `scaler`. + @test all(pbs_scaled./scaler .≈ pbs) + # And the band frequencies should all be scaled. + @test all(lower_bands(pbs_scaled)./scaler .≈ lower_bands(pbs)) + @test all(center_bands(pbs_scaled)./scaler .≈ center_bands(pbs)) + @test all(upper_bands(pbs_scaled)./scaler .≈ upper_bands(pbs)) + + # Creating a non-lazy version of the PBS should give the same stuff as the lazy version. + pbs_scaled_non_lazy = ProportionalBandSpectrum(typeof(center_bands(pbs_scaled)), center_bands(pbs_scaled)[begin], pbs_scaled, freq_scaler(pbs_scaled)) + @test all(pbs_scaled_non_lazy .≈ pbs_scaled) + @test lower_bands(pbs_scaled_non_lazy) === lower_bands(pbs_scaled) + @test center_bands(pbs_scaled_non_lazy) === center_bands(pbs_scaled) + @test upper_bands(pbs_scaled_non_lazy) === upper_bands(pbs_scaled) + end + + # Now, for the tonal stuff. + # Putting the narrowband frequencies on nice round numbers ends up being bad for the tonal case, since the tones can fall into different bands for different values of `scaler`, which leads to very different PBS values. + # So tweak those a bit. + freq_min_nb = 50.1 + nfreq = 951 + df_nb = 2.0 + f_nb = freq_min_nb .+ (0:nfreq-1) .* df_nb + psd = psd_func.(f_nb) + # pbs = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb, df_nb, psd) + msp = psd .* df_nb + scaler = 1 + tonal = true + pbs_tonal = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb, df_nb, msp, scaler, tonal) + # Narrowband frequencies go from 50 Hz to 1950 Hz, so check that. + cbands = center_bands(pbs_tonal) + @test band_start(cbands) == 17 + @test band_end(cbands) == 33 + + # Now make sure we get the right answer. + lbands = lower_bands(pbs_tonal) + ubands = upper_bands(pbs_tonal) + for (lband, uband, amp) in zip(lbands, ubands, pbs_tonal) + # First index we want in f_nb is the one that is greater than or equal to lband. + istart = searchsortedfirst(f_nb, lband) + # Last index we want in f_nb is th one that is less than or equal to uband. + iend = searchsortedlast(f_nb, uband; lt=<=) + # Now check that we get the right answer. + @test sum(msp[istart:iend]) ≈ amp + end + + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(freq_min_nb, df_nb, msp, center_bands(pbs_tonal), tonal) + @test all(pbs_init_cbands .≈ pbs_tonal) + + # Now for the scaler stuff, can use the same trick for the non-tonal. + for scaler in [0.1, 0.5, 1.0, 1.5, 2.0] + freq_min_nb_scaled = freq_min_nb*scaler + # freq_max_nb_scaled = freq_max_nb*scaler + df_nb_scaled = df_nb*scaler + msp_scaled = psd .* df_nb_scaled + pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp_scaled, scaler, tonal) + + # We've changed the frequencies, but not the PSD, so the scaled PBS should be the same as the original as long as we account for the different frequency bin widths via the `scaler`. + @test all(pbs_scaled./scaler .≈ pbs_tonal) + # And the band frequencies should all be scaled. + @test all(center_bands(pbs_scaled)./scaler .≈ center_bands(pbs_tonal)) + end + end + + @testset "spectrum, lowest narrowband on a right edge" begin + freq_min_nb = 55.0 + freq_max_nb = 1950.0 df_nb = 2.0 f_nb = freq_min_nb:df_nb:freq_max_nb psd = psd_func.(f_nb) - pbs = ProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb, df_nb, psd) + # pbs = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb, df_nb, psd) + msp = psd .* df_nb + pbs = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb, df_nb, msp) + + # Creating a non-lazy version of the PBS should give the same stuff as the lazy version. + pbs_non_lazy = ProportionalBandSpectrum(typeof(center_bands(pbs)), center_bands(pbs)[begin], pbs) + @test all(pbs_non_lazy .≈ pbs) + @test lower_bands(pbs_non_lazy) === lower_bands(pbs) + @test center_bands(pbs_non_lazy) === center_bands(pbs) + @test upper_bands(pbs_non_lazy) === upper_bands(pbs) + lbands = lower_bands(pbs) cbands = center_bands(pbs) ubands = upper_bands(pbs) @@ -1336,6 +2445,82 @@ end res = res_first + sum(psd[istart+1:iend-1].*df_nb) + res_last @test pbs_b ≈ res end + + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(freq_min_nb, df_nb, msp, center_bands(pbs)) + @test all(pbs_init_cbands .≈ pbs) + + # Now, check that the `scaler` argument works. + for scaler in [0.1, 0.5, 1.0, 1.5, 2.0] + freq_min_nb_scaled = freq_min_nb*scaler + freq_max_nb_scaled = freq_max_nb*scaler + df_nb_scaled = df_nb*scaler + # pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb_scaled, df_nb_scaled, psd, scaler) + # pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp, scaler) + msp_scaled = psd .* df_nb_scaled + pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp_scaled, scaler) + + # We've changed the frequencies, but not the PSD, so the scaled PBS should be the original PBS multipiled by `scaler`. + @test all(pbs_scaled./scaler .≈ pbs) + # And the band frequencies should all be scaled. + @test all(lower_bands(pbs_scaled)./scaler .≈ lower_bands(pbs)) + @test all(center_bands(pbs_scaled)./scaler .≈ center_bands(pbs)) + @test all(upper_bands(pbs_scaled)./scaler .≈ upper_bands(pbs)) + + # Creating a non-lazy version of the PBS should give the same stuff as the lazy version. + pbs_scaled_non_lazy = ProportionalBandSpectrum(typeof(center_bands(pbs_scaled)), center_bands(pbs_scaled)[begin], pbs_scaled, freq_scaler(pbs_scaled)) + @test all(pbs_scaled_non_lazy .≈ pbs_scaled) + @test lower_bands(pbs_scaled_non_lazy) === lower_bands(pbs_scaled) + @test center_bands(pbs_scaled_non_lazy) === center_bands(pbs_scaled) + @test upper_bands(pbs_scaled_non_lazy) === upper_bands(pbs_scaled) + end + + # Now, for the tonal stuff. + # Putting the narrowband frequencies on nice round numbers ends up being bad for the tonal case, since the tones can fall into different bands for different values of `scaler`, which leads to very different PBS values. + # So tweak those a bit. + freq_min_nb = 55.1 + df_nb = 2.0 + nfreq = length(f_nb) + f_nb = freq_min_nb .+ (0:nfreq-1) .* df_nb + psd = psd_func.(f_nb) + msp = psd .* df_nb + scaler = 1 + tonal = true + pbs_tonal = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb, df_nb, msp, scaler, tonal) + # Narrowband frequencies go from 55.1 Hz to 1949.1 Hz, so check that. + cbands = center_bands(pbs_tonal) + @test band_start(cbands) == 17 + @test band_end(cbands) == 33 + + # Now make sure we get the right answer. + lbands = lower_bands(pbs_tonal) + ubands = upper_bands(pbs_tonal) + for (lband, uband, amp) in zip(lbands, ubands, pbs_tonal) + # First index we want in f_nb is the one that is greater than or equal to lband. + istart = searchsortedfirst(f_nb, lband) + # Last index we want in f_nb is th one that is less than or equal to uband. + iend = searchsortedlast(f_nb, uband; lt=<=) + # Now check that we get the right answer. + @test sum(msp[istart:iend]) ≈ amp + end + + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(freq_min_nb, df_nb, msp, center_bands(pbs_tonal), tonal) + @test all(pbs_init_cbands .≈ pbs_tonal) + + # Now for the scaler stuff, can use the same trick for the non-tonal. + for scaler in [0.1, 0.5, 1.0, 1.5, 2.0] + freq_min_nb_scaled = freq_min_nb*scaler + # freq_max_nb_scaled = freq_max_nb*scaler + df_nb_scaled = df_nb*scaler + msp_scaled = psd .* df_nb_scaled + pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp_scaled, scaler, tonal) + + # We've changed the frequencies, but not the PSD, so the scaled PBS should be the same as the original as long as we account for the different frequency bin widths via the `scaler`. + @test all(pbs_scaled./scaler .≈ pbs_tonal) + # And the band frequencies should all be scaled. + @test all(center_bands(pbs_scaled)./scaler .≈ center_bands(pbs_tonal)) + end end @testset "spectrum, lowest narrowband on a left edge" begin @@ -1344,7 +2529,16 @@ end df_nb = 2.0 f_nb = freq_min_nb:df_nb:freq_max_nb psd = psd_func.(f_nb) - pbs = ProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb, df_nb, psd) + msp = psd .* df_nb + pbs = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb, df_nb, msp) + + # Creating a non-lazy version of the PBS should give the same stuff as the lazy version. + pbs_non_lazy = ProportionalBandSpectrum(typeof(center_bands(pbs)), center_bands(pbs)[begin], pbs) + @test all(pbs_non_lazy .≈ pbs) + @test lower_bands(pbs_non_lazy) === lower_bands(pbs) + @test center_bands(pbs_non_lazy) === center_bands(pbs) + @test upper_bands(pbs_non_lazy) === upper_bands(pbs) + lbands = lower_bands(pbs) cbands = center_bands(pbs) ubands = upper_bands(pbs) @@ -1370,6 +2564,94 @@ end res = res_first + sum(psd[istart+1:iend-1].*df_nb) + res_last @test pbs_b ≈ res end + + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(freq_min_nb, df_nb, msp, center_bands(pbs)) + @test all(pbs_init_cbands .≈ pbs) + + # Now, check that the `scaler` argument works. + for scaler in [0.1, 0.5, 1.0, 1.5, 2.0] + freq_min_nb_scaled = freq_min_nb*scaler + freq_max_nb_scaled = freq_max_nb*scaler + df_nb_scaled = df_nb*scaler + # pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb_scaled, df_nb_scaled, psd, scaler) + # pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp, scaler) + msp_scaled = psd .* df_nb_scaled + pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp_scaled, scaler) + + # We've changed the frequencies, but not the PSD, so the scaled PBS should be the original PBS multipiled by `scaler`. + if length(pbs_scaled) == length(pbs) + @test all(pbs_scaled./scaler .≈ pbs) + # And the band frequencies should all be scaled. + @test all(lower_bands(pbs_scaled)./scaler .≈ lower_bands(pbs)) + @test all(center_bands(pbs_scaled)./scaler .≈ center_bands(pbs)) + @test all(upper_bands(pbs_scaled)./scaler .≈ upper_bands(pbs)) + else + + # But because of numerical stuff, the unscaled test above grabbed a lower frequency band that it shouldn't have or whatever, so we have to skip that one. + # And test that the "extra" band at the beginning is essentially zero. + @test pbs[1] ≈ 0 + @test all(pbs_scaled./scaler .≈ pbs[2:end]) + # And the band frequencies should all be scaled. + @test all(lower_bands(pbs_scaled)./scaler .≈ lower_bands(pbs)[2:end]) + @test all(center_bands(pbs_scaled)./scaler .≈ center_bands(pbs)[2:end]) + @test all(upper_bands(pbs_scaled)./scaler .≈ upper_bands(pbs)[2:end]) + end + + # Creating a non-lazy version of the PBS should give the same stuff as the lazy version. + pbs_scaled_non_lazy = ProportionalBandSpectrum(typeof(center_bands(pbs_scaled)), center_bands(pbs_scaled)[begin], pbs_scaled, freq_scaler(pbs_scaled)) + @test all(pbs_scaled_non_lazy .≈ pbs_scaled) + @test lower_bands(pbs_scaled_non_lazy) === lower_bands(pbs_scaled) + @test center_bands(pbs_scaled_non_lazy) === center_bands(pbs_scaled) + @test upper_bands(pbs_scaled_non_lazy) === upper_bands(pbs_scaled) + end + + # Now, for the tonal stuff. + # Putting the narrowband frequencies on nice round numbers ends up being bad for the tonal case, since the tones can fall into different bands for different values of `scaler`, which leads to very different PBS values. + # So tweak those a bit. + freq_min_nb = 57.1 + df_nb = 2.0 + nfreq = 947 + f_nb = freq_min_nb .+ (0:nfreq-1) .* df_nb + psd = psd_func.(f_nb) + msp = psd .* df_nb + scaler = 1 + tonal = true + pbs_tonal = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb, df_nb, msp, scaler, tonal) + # Narrowband frequencies go from 57.1 Hz to 1949.1 Hz, so check that. + cbands = center_bands(pbs_tonal) + @test band_start(cbands) == 18 + @test band_end(cbands) == 33 + + # Now make sure we get the right answer. + lbands = lower_bands(pbs_tonal) + ubands = upper_bands(pbs_tonal) + for (lband, uband, amp) in zip(lbands, ubands, pbs_tonal) + # First index we want in f_nb is the one that is greater than or equal to lband. + istart = searchsortedfirst(f_nb, lband) + # Last index we want in f_nb is th one that is less than or equal to uband. + iend = searchsortedlast(f_nb, uband; lt=<=) + # Now check that we get the right answer. + @test sum(msp[istart:iend]) ≈ amp + end + + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(freq_min_nb, df_nb, msp, center_bands(pbs_tonal), tonal) + @test all(pbs_init_cbands .≈ pbs_tonal) + + # Now for the scaler stuff, can use the same trick for the non-tonal. + for scaler in [0.1, 0.5, 1.0, 1.5, 2.0] + freq_min_nb_scaled = freq_min_nb*scaler + # freq_max_nb_scaled = freq_max_nb*scaler + df_nb_scaled = df_nb*scaler + msp_scaled = psd .* df_nb_scaled + pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp_scaled, scaler, tonal) + + # We've changed the frequencies, but not the PSD, so the scaled PBS should be the same as the original as long as we account for the different frequency bin widths via the `scaler`. + @test all(pbs_scaled./scaler .≈ pbs_tonal) + # And the band frequencies should all be scaled. + @test all(center_bands(pbs_scaled)./scaler .≈ center_bands(pbs_tonal)) + end end @testset "spectrum, highest narrowband on a right edge" begin @@ -1378,7 +2660,18 @@ end df_nb = 2.0 f_nb = freq_min_nb:df_nb:freq_max_nb psd = psd_func.(f_nb) - pbs = ProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb, df_nb, psd) + # pbs = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb, df_nb, psd) + msp = psd .* df_nb + pbs = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb, df_nb, msp) + + + # Creating a non-lazy version of the PBS should give the same stuff as the lazy version. + pbs_non_lazy = ProportionalBandSpectrum(typeof(center_bands(pbs)), center_bands(pbs)[begin], pbs) + @test all(pbs_non_lazy .≈ pbs) + @test lower_bands(pbs_non_lazy) === lower_bands(pbs) + @test center_bands(pbs_non_lazy) === center_bands(pbs) + @test upper_bands(pbs_non_lazy) === upper_bands(pbs) + lbands = lower_bands(pbs) cbands = center_bands(pbs) ubands = upper_bands(pbs) @@ -1402,6 +2695,82 @@ end res = res_first + sum(psd[istart+1:iend-1].*df_nb) + res_last @test pbs_b ≈ res end + + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(freq_min_nb, df_nb, msp, center_bands(pbs)) + @test all(pbs_init_cbands .≈ pbs) + + # Now, check that the `scaler` argument works. + for scaler in [0.1, 0.5, 1.0, 1.5, 2.0] + freq_min_nb_scaled = freq_min_nb*scaler + freq_max_nb_scaled = freq_max_nb*scaler + df_nb_scaled = df_nb*scaler + # pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb_scaled, df_nb_scaled, psd, scaler) + # pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp, scaler) + msp_scaled = psd .* df_nb_scaled + pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp_scaled, scaler) + + # We've changed the frequencies, but not the PSD, so the scaled PBS should be the original PBS multipiled by `scaler`. + @test all(pbs_scaled./scaler .≈ pbs) + # And the band frequencies should all be scaled. + @test all(lower_bands(pbs_scaled)./scaler .≈ lower_bands(pbs)) + @test all(center_bands(pbs_scaled)./scaler .≈ center_bands(pbs)) + @test all(upper_bands(pbs_scaled)./scaler .≈ upper_bands(pbs)) + + # Creating a non-lazy version of the PBS should give the same stuff as the lazy version. + pbs_scaled_non_lazy = ProportionalBandSpectrum(typeof(center_bands(pbs_scaled)), center_bands(pbs_scaled)[begin], pbs_scaled, freq_scaler(pbs_scaled)) + @test all(pbs_scaled_non_lazy .≈ pbs_scaled) + @test lower_bands(pbs_scaled_non_lazy) === lower_bands(pbs_scaled) + @test center_bands(pbs_scaled_non_lazy) === center_bands(pbs_scaled) + @test upper_bands(pbs_scaled_non_lazy) === upper_bands(pbs_scaled) + end + + # Now, for the tonal stuff. + # Putting the narrowband frequencies on nice round numbers ends up being bad for the tonal case, since the tones can fall into different bands for different values of `scaler`, which leads to very different PBS values. + # So tweak those a bit. + freq_min_nb = 50.1 + df_nb = 2.0 + nfreq = length(f_nb) + f_nb = freq_min_nb .+ (0:nfreq-1) .* df_nb + psd = psd_func.(f_nb) + msp = psd .* df_nb + scaler = 1 + tonal = true + pbs_tonal = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb, df_nb, msp, scaler, tonal) + # Narrowband frequencies go from 50.1 Hz to 1798.1 Hz, so check that. + cbands = center_bands(pbs_tonal) + @test band_start(cbands) == 17 + @test band_end(cbands) == 32 + + # Now make sure we get the right answer. + lbands = lower_bands(pbs_tonal) + ubands = upper_bands(pbs_tonal) + for (lband, uband, amp) in zip(lbands, ubands, pbs_tonal) + # First index we want in f_nb is the one that is greater than or equal to lband. + istart = searchsortedfirst(f_nb, lband) + # Last index we want in f_nb is th one that is less than or equal to uband. + iend = searchsortedlast(f_nb, uband; lt=<=) + # Now check that we get the right answer. + @test sum(msp[istart:iend]) ≈ amp + end + + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(freq_min_nb, df_nb, msp, center_bands(pbs_tonal), tonal) + @test all(pbs_init_cbands .≈ pbs_tonal) + + # Now for the scaler stuff, can use the same trick for the non-tonal. + for scaler in [0.1, 0.5, 1.0, 1.5, 2.0] + freq_min_nb_scaled = freq_min_nb*scaler + # freq_max_nb_scaled = freq_max_nb*scaler + df_nb_scaled = df_nb*scaler + msp_scaled = psd .* df_nb_scaled + pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp_scaled, scaler, tonal) + + # We've changed the frequencies, but not the PSD, so the scaled PBS should be the same as the original as long as we account for the different frequency bin widths via the `scaler`. + @test all(pbs_scaled./scaler .≈ pbs_tonal) + # And the band frequencies should all be scaled. + @test all(center_bands(pbs_scaled)./scaler .≈ center_bands(pbs_tonal)) + end end @testset "spectrum, highest narrowband on a left edge" begin @@ -1410,7 +2779,17 @@ end df_nb = 2.0 f_nb = freq_min_nb:df_nb:freq_max_nb psd = psd_func.(f_nb) - pbs = ProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb, df_nb, psd) + # pbs = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb, df_nb, psd) + msp = psd .* df_nb + pbs = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb, df_nb, msp) + + # Creating a non-lazy version of the PBS should give the same stuff as the lazy version. + pbs_non_lazy = ProportionalBandSpectrum(typeof(center_bands(pbs)), center_bands(pbs)[begin], pbs) + @test all(pbs_non_lazy .≈ pbs) + @test lower_bands(pbs_non_lazy) === lower_bands(pbs) + @test center_bands(pbs_non_lazy) === center_bands(pbs) + @test upper_bands(pbs_non_lazy) === upper_bands(pbs) + lbands = lower_bands(pbs) cbands = center_bands(pbs) ubands = upper_bands(pbs) @@ -1434,6 +2813,1680 @@ end res = res_first + sum(psd[istart+1:iend-1].*df_nb) + res_last @test pbs_b ≈ res end + + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(freq_min_nb, df_nb, msp, center_bands(pbs)) + @test all(pbs_init_cbands .≈ pbs) + + # Now, check that the `scaler` argument works. + for scaler in [0.1, 0.5, 1.0, 1.5, 2.0] + freq_min_nb_scaled = freq_min_nb*scaler + freq_max_nb_scaled = freq_max_nb*scaler + df_nb_scaled = df_nb*scaler + # pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb_scaled, df_nb_scaled, psd, scaler) + # pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp, scaler) + msp_scaled = psd .* df_nb_scaled + pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp_scaled, scaler) + + # We've changed the frequencies, but not the PSD, so the scaled PBS should be the original PBS multipiled by `scaler`. + @test all(pbs_scaled./scaler .≈ pbs) + # And the band frequencies should all be scaled. + @test all(lower_bands(pbs_scaled)./scaler .≈ lower_bands(pbs)) + @test all(center_bands(pbs_scaled)./scaler .≈ center_bands(pbs)) + @test all(upper_bands(pbs_scaled)./scaler .≈ upper_bands(pbs)) + + # Creating a non-lazy version of the PBS should give the same stuff as the lazy version. + pbs_scaled_non_lazy = ProportionalBandSpectrum(typeof(center_bands(pbs_scaled)), center_bands(pbs_scaled)[begin], pbs_scaled, freq_scaler(pbs_scaled)) + @test all(pbs_scaled_non_lazy .≈ pbs_scaled) + @test lower_bands(pbs_scaled_non_lazy) === lower_bands(pbs_scaled) + @test center_bands(pbs_scaled_non_lazy) === center_bands(pbs_scaled) + @test upper_bands(pbs_scaled_non_lazy) === upper_bands(pbs_scaled) + end + + # Now, for the tonal stuff. + # Putting the narrowband frequencies on nice round numbers ends up being bad for the tonal case, since the tones can fall into different bands for different values of `scaler`, which leads to very different PBS values. + # So tweak those a bit. + freq_min_nb = 50.1 + df_nb = 2.0 + nfreq = length(f_nb) + f_nb = freq_min_nb .+ (0:nfreq-1) .* df_nb + psd = psd_func.(f_nb) + msp = psd .* df_nb + scaler = 1 + tonal = true + pbs_tonal = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb, df_nb, msp, scaler, tonal) + # Narrowband frequencies go from 50.1 Hz to 1800.1 Hz, so check that. + cbands = center_bands(pbs_tonal) + @test band_start(cbands) == 17 + @test band_end(cbands) == 33 + + # Now make sure we get the right answer. + lbands = lower_bands(pbs_tonal) + ubands = upper_bands(pbs_tonal) + for (lband, uband, amp) in zip(lbands, ubands, pbs_tonal) + # First index we want in f_nb is the one that is greater than or equal to lband. + istart = searchsortedfirst(f_nb, lband) + # Last index we want in f_nb is th one that is less than or equal to uband. + iend = searchsortedlast(f_nb, uband; lt=<=) + # Now check that we get the right answer. + @test sum(msp[istart:iend]) ≈ amp + end + + # Make sure I get the same thing if I pass in an initialized proportional center band object. + pbs_init_cbands = LazyNBProportionalBandSpectrum(freq_min_nb, df_nb, msp, center_bands(pbs_tonal), tonal) + @test all(pbs_init_cbands .≈ pbs_tonal) + + # Now for the scaler stuff, can use the same trick for the non-tonal. + for scaler in [0.1, 0.5, 1.0, 1.5, 2.0] + freq_min_nb_scaled = freq_min_nb*scaler + # freq_max_nb_scaled = freq_max_nb*scaler + df_nb_scaled = df_nb*scaler + msp_scaled = psd .* df_nb_scaled + pbs_scaled = LazyNBProportionalBandSpectrum(ApproximateThirdOctaveBands, freq_min_nb_scaled, df_nb_scaled, msp_scaled, scaler, tonal) + + # We've changed the frequencies, but not the PSD, so the scaled PBS should be the same as the original as long as we account for the different frequency bin widths via the `scaler`. + @test all(pbs_scaled./scaler .≈ pbs_tonal) + # And the band frequencies should all be scaled. + @test all(center_bands(pbs_scaled)./scaler .≈ center_bands(pbs_tonal)) + end + end + end + + @testset "lazy PBS ProportionalBandSpectrum" begin + @testset "same bands" begin + for TPB in [ExactProportionalBands{3}, ExactProportionalBands{1}, ExactProportionalBands{12}, + ApproximateThirdOctaveBands, ApproximateOctaveBands] + cbands1 = TPB{:center}(10, 16) + pbs1 = ProportionalBandSpectrum(rand(length(cbands1)), cbands1) + cbands2 = TPB{ :center}(10, 16) + pbs2 = LazyPBSProportionalBandSpectrum(pbs1, cbands2) + @test all(pbs2 .≈ pbs1) + end + end + + @testset "shift bands by whole indices" begin + for TPB in [ExactProportionalBands{3}, ExactProportionalBands{1}, ExactProportionalBands{12}, + ApproximateThirdOctaveBands, ApproximateOctaveBands] + cbands1 = TPB{:center}(10, 16) + pbs1 = ProportionalBandSpectrum(rand(length(cbands1)), cbands1) + cbands2 = TPB{:center}(9, 17) + pbs2 = LazyPBSProportionalBandSpectrum(pbs1, cbands2) + @test pbs2[begin] ≈ 0 + @test all(pbs2[begin+1:end-1] .≈ pbs1) + @test pbs2[end] ≈ 0 + end + end + + @testset "shift bands up by non-whole indices" begin + for TPB in [ExactProportionalBands{3}, ExactProportionalBands{1}, ExactProportionalBands{12}, + ApproximateThirdOctaveBands, ApproximateOctaveBands] + cbands1 = TPB{:center}(10, 16) + pbs1 = ProportionalBandSpectrum(rand(length(cbands1)), cbands1) + scaler2 = 1.01 + cbands2 = TPB{:center}(10, 16, scaler2) + pbs2 = LazyPBSProportionalBandSpectrum(pbs1, cbands2) + lbands1 = lower_bands(pbs1) + ubands1 = upper_bands(pbs1) + lbands2 = lower_bands(pbs2) + ubands2 = upper_bands(pbs2) + for i in 1:length(pbs1) + if i < length(pbs1) + amp2_left = pbs1[i]/(ubands1[i] - lbands1[i])*(ubands1[i] - lbands2[i]) + amp2_right = pbs1[i+1]/(ubands1[i+1] - lbands1[i+1])*(ubands2[i] - lbands1[i+1]) + amp2_check = amp2_left + amp2_right + else + amp2_check = pbs1[i]/(ubands1[i] - lbands1[i])*(ubands1[i] - lbands2[i]) + end + @test pbs2[i] ≈ amp2_check + end + end + end + + @testset "shift bands down by non-whole indices" begin + for TPB in [ExactProportionalBands{3}, ExactProportionalBands{1}, ExactProportionalBands{12}, + ApproximateThirdOctaveBands, ApproximateOctaveBands] + cbands1 = TPB{:center}(10, 16) + pbs1 = ProportionalBandSpectrum(rand(length(cbands1)), cbands1) + scaler2 = 0.99 + cbands2 = TPB{:center}(10, 16, scaler2) + pbs2 = LazyPBSProportionalBandSpectrum(pbs1, cbands2) + lbands1 = lower_bands(pbs1) + ubands1 = upper_bands(pbs1) + lbands2 = lower_bands(pbs2) + ubands2 = upper_bands(pbs2) + for i in 1:length(pbs1) + if i > 1 + amp2_left = pbs1[i-1]/(ubands1[i-1] - lbands1[i-1])*(ubands1[i-1] - lbands2[i]) + amp2_right = pbs1[i]/(ubands1[i] - lbands1[i])*(ubands2[i] - lbands1[i]) + amp2_check = amp2_left + amp2_right + else + amp2_check = pbs1[i]/(ubands1[i] - lbands1[i])*(ubands2[i] - lbands1[i]) + end + @test pbs2[i] ≈ amp2_check + end + end + end + + @testset "output bands too low" begin + for TPB in [ExactProportionalBands{3}, ExactProportionalBands{1}, ExactProportionalBands{12}, + ApproximateThirdOctaveBands, ApproximateOctaveBands] + cbands1 = TPB{:center}(10, 16) + pbs1 = ProportionalBandSpectrum(rand(length(cbands1)), cbands1) + cbands2 = TPB{:center}(1, 9) + pbs2 = LazyPBSProportionalBandSpectrum(pbs1, cbands2) + @test all(pbs2 .≈ 0) + end + end + + @testset "output bands too high" begin + for TPB in [ExactProportionalBands{3}, ExactProportionalBands{1}, ExactProportionalBands{12}, + ApproximateThirdOctaveBands, ApproximateOctaveBands] + cbands1 = TPB{:center}(10, 16) + pbs1 = ProportionalBandSpectrum(rand(length(cbands1)), cbands1) + cbands2 = TPB{:center}(17, 20) + pbs2 = LazyPBSProportionalBandSpectrum(pbs1, cbands2) + @test all(pbs2 .≈ 0) + end + end + + @testset "input 3rd-octave, output octave, aligned" begin + cbands1 = ExactProportionalBands{3,:center}(32, 49) + pbs1 = ProportionalBandSpectrum(rand(length(cbands1)), cbands1) + pbs2 = LazyPBSProportionalBandSpectrum(ExactProportionalBands{1}, pbs1) + cbands2 = center_bands(pbs2) + @test band_start(cbands2) == 11 + @test band_end(cbands2) == 16 + for i in 1:length(pbs2) + j = (i-1)*3 + 1 + @test pbs2[i] ≈ sum(pbs1[j:j+2]) + end + end + + @testset "input octave, output 3rd-octave, aligned" begin + cbands1 = ExactProportionalBands{1,:center}(11, 16) + pbs1 = ProportionalBandSpectrum(rand(length(cbands1)), cbands1) + pbs2 = LazyPBSProportionalBandSpectrum(ExactProportionalBands{3}, pbs1) + cbands2 = center_bands(pbs2) + @test band_start(cbands2) == 32 + @test band_end(cbands2) == 49 + + lbands1 = lower_bands(pbs1) + ubands1 = upper_bands(pbs1) + lbands2 = lower_bands(pbs2) + ubands2 = upper_bands(pbs2) + + for i in 1:length(pbs1) + j = (i-1)*3 + 1 + + @test pbs2[j] ≈ pbs1[i]/(ubands1[i] - lbands1[i])*(ubands2[j] - lbands2[j]) + @test pbs2[j+1] ≈ pbs1[i]/(ubands1[i] - lbands1[i])*(ubands2[j+1] - lbands2[j+1]) + @test pbs2[j+2] ≈ pbs1[i]/(ubands1[i] - lbands1[i])*(ubands2[j+2] - lbands2[j+2]) + end + end + + @testset "input 3rd-octave, output octave, not aligned, scaled up" begin + cbands1 = ExactProportionalBands{3,:center}(32, 49) + pbs1 = ProportionalBandSpectrum(rand(length(cbands1)), cbands1) + cbands2 = ExactProportionalBands{1,:center}(11, 16, 1.01) + # pbs2 = LazyPBSProportionalBandSpectrum(ExactProportionalBands{1}, pbs1, 1.01) + pbs2 = LazyPBSProportionalBandSpectrum(pbs1, cbands2) + lbands1 = lower_bands(pbs1) + ubands1 = upper_bands(pbs1) + lbands2 = lower_bands(pbs2) + ubands2 = upper_bands(pbs2) + for i in 1:length(pbs2) + if i < length(pbs2) + # | . . | . . | + # | | | + j = (i-1)*3 + 1 + amp2_left = (pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - lbands2[i]) + + pbs1[j+1] + + pbs1[j+2]) + + j = (i)*3 + 1 + amp2_right = pbs1[j]/(ubands1[j] - lbands1[j])*(ubands2[i] - lbands1[j]) + + amp2_check = amp2_left + amp2_right + else + j = (i-1)*3 + 1 + amp2_check = (pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - lbands2[i]) + + pbs1[j+1] + + pbs1[j+2]) + + + end + @test pbs2[i] ≈ amp2_check + end + end + + @testset "input 3rd-octave, output octave, not aligned, scaled down" begin + cbands1 = ExactProportionalBands{3,:center}(32, 49) + pbs1 = ProportionalBandSpectrum(rand(length(cbands1)), cbands1) + cbands2 = ExactProportionalBands{1,:center}(11, 16, 0.99) + pbs2 = LazyPBSProportionalBandSpectrum(pbs1, cbands2) + lbands1 = lower_bands(pbs1) + ubands1 = upper_bands(pbs1) + lbands2 = lower_bands(pbs2) + ubands2 = upper_bands(pbs2) + for i in 1:length(pbs2) + j = (i-1)*3 + 1 + if i > 1 + # | . . | . . | + # | | | + amp2_left = pbs1[j-1]/(ubands1[j-1] - lbands1[j-1])*(ubands1[j-1] - lbands2[i]) + amp2_right = pbs1[j] + pbs1[j+1] + pbs1[j+2]/(ubands1[j+2] - lbands1[j+2])*(ubands2[i] - lbands1[j+2]) + amp2_check = amp2_left + amp2_right + else + amp2_right = pbs1[j] + pbs1[j+1] + pbs1[j+2]/(ubands1[j+2] - lbands1[j+2])*(ubands2[i] - lbands1[j+2]) + amp2_check = amp2_right + end + @test pbs2[i] ≈ amp2_check + end + end + + @testset "input octave, output 3rd-octave, not aligned, scaled up" begin + cbands1 = ExactProportionalBands{1,:center}(11, 16) + pbs1 = ProportionalBandSpectrum(rand(length(cbands1)), cbands1) + cbands2 = ExactProportionalBands{3, :center}(32, 49, 1.01) + pbs2 = LazyPBSProportionalBandSpectrum(pbs1, cbands2) + + lbands1 = lower_bands(pbs1) + ubands1 = upper_bands(pbs1) + lbands2 = lower_bands(pbs2) + ubands2 = upper_bands(pbs2) + + for i in 1:length(pbs1) + # | | | + # | . . | . . | + j = 3*(i - 1) + 1 + @test pbs2[j] ≈ pbs1[i]/(ubands1[i] - lbands1[i])*(ubands2[j] - lbands2[j]) + @test pbs2[j+1] ≈ pbs1[i]/(ubands1[i] - lbands1[i])*(ubands2[j+1] - lbands2[j+1]) + if i < length(pbs1) + @test pbs2[j+2] ≈ ( + pbs1[i]/(ubands1[i] - lbands1[i])*(ubands1[i] - lbands2[j+2]) + + pbs1[i+1]/(ubands1[i+1] - lbands1[i+1])*(ubands2[j+2] - lbands1[i+1])) + else + @test pbs2[j+2] ≈ pbs1[i]/(ubands1[i] - lbands1[i])*(ubands1[i] - lbands2[j+2]) + end + end + end + + @testset "input octave, output 3rd-octave, not aligned, scaled down" begin + cbands1 = ExactProportionalBands{1,:center}(11, 16) + pbs1 = ProportionalBandSpectrum(rand(length(cbands1)), cbands1) + cbands2 = ExactProportionalBands{3, :center}(32, 49, 0.99) + pbs2 = LazyPBSProportionalBandSpectrum(pbs1, cbands2) + + lbands1 = lower_bands(pbs1) + ubands1 = upper_bands(pbs1) + lbands2 = lower_bands(pbs2) + ubands2 = upper_bands(pbs2) + + for i in 1:length(pbs1) + # | | | + # | . . | . . | + j = 3*(i - 1) + 1 + if i > 1 + @test pbs2[j] ≈ ( + pbs1[i-1]/(ubands1[i-1] - lbands1[i-1])*(ubands1[i-1] - lbands2[j]) + + pbs1[i]/(ubands1[i] - lbands1[i])*(ubands2[j] - lbands1[i])) + else + @test pbs2[j] ≈ pbs1[i]/(ubands1[i] - lbands1[i])*(ubands2[j] - lbands1[i]) + end + @test pbs2[j+1] ≈ pbs1[i]/(ubands1[i] - lbands1[i])*(ubands2[j+1] - lbands2[j+1]) + @test pbs2[j+2] ≈ pbs1[i]/(ubands1[i] - lbands1[i])*(ubands2[j+2] - lbands2[j+2]) + end + end + + end + + @testset "combining proportional band spectrums" begin + + @testset "same bands" begin + nfreq_nb = 800 + freq_min_nb = 55.0 + freq_max_nb = 1950.0 + df_nb = (freq_max_nb - freq_min_nb)/(nfreq_nb - 1) + f_nb = freq_min_nb .+ (0:(nfreq_nb-1)).*df_nb + psd = psd_func.(f_nb) + msp = psd .* df_nb + for TPB in [ExactProportionalBands{3}, ExactProportionalBands{1}, ExactProportionalBands{12}, ApproximateThirdOctaveBands, ApproximateOctaveBands] + # pbs1_lazy = LazyNBProportionalBandSpectrum(TPB, freq_min_nb, df_nb, psd) + pbs1_lazy = LazyNBProportionalBandSpectrum(TPB, freq_min_nb, df_nb, msp) + pbs1 = ProportionalBandSpectrum(collect(pbs1_lazy), center_bands(pbs1_lazy)) + pbs2 = ProportionalBandSpectrum(collect(pbs1_lazy), center_bands(pbs1_lazy)) + pbs3 = ProportionalBandSpectrum(collect(pbs1_lazy), center_bands(pbs1_lazy)) + + # So, when we add these, the proportional band spectrum should be just 3 times whatever the original was, and all the bands should be the same. + pbs_combined = combine([pbs1, pbs2, pbs3], center_bands(pbs1_lazy)) + @test lower_bands(pbs_combined) == lower_bands(pbs1_lazy) + @test center_bands(pbs_combined) == center_bands(pbs1_lazy) + @test upper_bands(pbs_combined) == upper_bands(pbs1_lazy) + @test all(pbs_combined .≈ (3 .* pbs1_lazy)) + end + end + + @testset "outbands lower than all inbands" begin + nfreq_nb = 800 + freq_min_nb = 55.0 + freq_max_nb = 1950.0 + df_nb = (freq_max_nb - freq_min_nb)/(nfreq_nb - 1) + f_nb = freq_min_nb .+ (0:(nfreq_nb-1)).*df_nb + psd = psd_func.(f_nb) + msp = psd .* df_nb + + for TPB in [ExactProportionalBands{3}, ExactProportionalBands{1}, ExactProportionalBands{12}, ApproximateThirdOctaveBands, ApproximateOctaveBands] + # pbs1_lazy = LazyNBProportionalBandSpectrum(TPB, freq_min_nb, df_nb, psd) + pbs1_lazy = LazyNBProportionalBandSpectrum(TPB, freq_min_nb, df_nb, msp) + pbs1 = ProportionalBandSpectrum(collect(pbs1_lazy), center_bands(pbs1_lazy)) + pbs2 = ProportionalBandSpectrum(collect(pbs1_lazy), center_bands(pbs1_lazy)) + pbs3 = ProportionalBandSpectrum(collect(pbs1_lazy), center_bands(pbs1_lazy)) + + # outcbands = ExactProportionalBands{3, :center}(10, 16) + outcbands = TPB{:center}(2.0, 10.0) + # Make sure the outbands are actually all lower than the input narrowbands. + @test last(upper_bands(outcbands)) < freq_min_nb - 0.5*df_nb + pbs_combined = combine([pbs1, pbs2, pbs3], outcbands) + @test center_bands(pbs_combined) == outcbands + @test all(pbs_combined .≈ 0) + end + end + + @testset "outbands higher than all inbands" begin + nfreq_nb = 800 + freq_min_nb = 55.0 + freq_max_nb = 1950.0 + df_nb = (freq_max_nb - freq_min_nb)/(nfreq_nb - 1) + f_nb = freq_min_nb .+ (0:(nfreq_nb-1)).*df_nb + psd = psd_func.(f_nb) + msp = psd .* df_nb + for TPB in [ExactProportionalBands{3}, ExactProportionalBands{1}, ExactProportionalBands{12}, ApproximateThirdOctaveBands, ApproximateOctaveBands] + pbs1_lazy = LazyNBProportionalBandSpectrum(ExactProportionalBands{3}, freq_min_nb, df_nb, msp) + pbs1 = ProportionalBandSpectrum(collect(pbs1_lazy), center_bands(pbs1_lazy)) + pbs2 = ProportionalBandSpectrum(collect(pbs1_lazy), center_bands(pbs1_lazy)) + pbs3 = ProportionalBandSpectrum(collect(pbs1_lazy), center_bands(pbs1_lazy)) + + outcbands = TPB{:center}(3000.0, 20000.0) + # Make sure the outbands are actually all higher than the input narrowbands. + @test first(lower_bands(outcbands)) > freq_max_nb + 0.5*df_nb + pbs_combined = combine([pbs1, pbs2, pbs3], outcbands) + @test center_bands(pbs_combined) == outcbands + @test all(pbs_combined .≈ 0) + end + end + + @testset "inbands lined up with outbands" begin + for TPB in [ExactProportionalBands{3}, ExactProportionalBands{1}, ExactProportionalBands{12}, ApproximateThirdOctaveBands, ApproximateOctaveBands] + cbands1 = TPB{:center}(10, 16) + pbs1 = ProportionalBandSpectrum(rand(length(cbands1)), cbands1) + cbands2 = TPB{:center}(11, 16) + pbs2 = ProportionalBandSpectrum(rand(length(cbands2)), cbands2) + cbands3 = TPB{:center}(12, 16) + pbs3 = ProportionalBandSpectrum(rand(length(cbands3)), cbands3) + + # outcbands = ExactProportionalBands{3, :center}(10, 16) + outcbands = TPB{:center}(10, 16) + pbs_combined = combine([pbs1, pbs2, pbs3], outcbands) + @test pbs_combined[1] ≈ pbs1[1] + @test pbs_combined[2] ≈ pbs1[2] + pbs2[1] + @test all(pbs_combined[3:end] .≈ pbs1[3:end] .+ pbs2[2:end] .+ pbs3) + end + end + + @testset "scaled outbands" begin + # Since proportional bands are... proportional, we can be clever about the scaler argument. + # For example, for a 1/3 octave band, log2(f_center2/f_center1) = (1/3), where f_center1 is a center frequency, and f_center2 is the next highest center frequency after f_center1. + # So, log2(f_center2) - log2(f_center1) = 1/3 + # log2(f_center2) = log2(f_center1) + 1/3 + # f_center2 = 2^(log2(f_center1) + 1/3) = f_center1*2^(1/3) + # So if I set the scaler argument to 2^(1/3), that should have the effect of shifting the frequency bands up one unscaled band. + # And if I do that twice (i.e., squaring the scaler), that should shift the frequency bands by two. + # But this doesn't work with the approximate bands, since those aren't exactly proportional bands. + for TPB in [ExactProportionalBands{3}, ExactProportionalBands{1}, ExactProportionalBands{12}, + # ApproximateThirdOctaveBands, ApproximateOctaveBands + ] + # cbands1 = ExactProportionalBands{3, :center}(10, 16) + cbands1 = TPB{:center}(10, 16) + pbs1 = ProportionalBandSpectrum(rand(length(cbands1)), cbands1) + # NO = octave_fraction(cbands1) + scaler = cbands1[2]/cbands1[1] + # cbands2 = ExactProportionalBands{3, :center}(10, 15, scaler) + cbands2 = TPB{:center}(10, 15, scaler) + pbs2 = ProportionalBandSpectrum(rand(length(cbands2)), cbands2) + scaler = cbands1[3]/cbands1[1] + # cbands3 = ExactProportionalBands{3, :center}(10, 14, scaler) + cbands3 = TPB{:center}(10, 14, scaler) + pbs3 = ProportionalBandSpectrum(rand(length(cbands3)), cbands3) + + # outcbands = ExactProportionalBands{3, :center}(10, 16) + outcbands = TPB{:center}(10, 16) + pbs_combined = combine([pbs1, pbs2, pbs3], outcbands) + @test pbs_combined[1] ≈ pbs1[1] + @test pbs_combined[2] ≈ pbs1[2] + pbs2[1] + @test all(pbs_combined[3:end] .≈ pbs1[3:end] .+ pbs2[2:end] .+ pbs3) + end + end + + @testset "non-aligned outbands, one input spectrum" begin + for TPB in [ExactProportionalBands{3}, ExactProportionalBands{1}, ExactProportionalBands{12}, + ApproximateThirdOctaveBands, ApproximateOctaveBands + ] + # cbands1 = ExactProportionalBands{3, :center}(10, 16) + cbands1 = TPB{:center}(10, 16) + lbands1 = lower_bands(cbands1) + ubands1 = upper_bands(cbands1) + pbs1 = ProportionalBandSpectrum(rand(length(cbands1)), cbands1) + + # outcbands = ExactProportionalBands{3, :center}(10, 16, 1.1) + # Need to make sure the frequency shift (here `1.05`) is small enough to shift the frequency bands by less than 1. + # `1.1` was too big for the 12th-octave bands. + outcbands = TPB{:center}(10, 16, 1.05) + outlbands = lower_bands(outcbands) + outubands = upper_bands(outcbands) + pbs_combined = combine([pbs1], outcbands) + for i in 1:length(pbs_combined)-1 + @test pbs_combined[i] ≈ ( + pbs1[i]/(ubands1[i] - lbands1[i])*(ubands1[i] - outlbands[i]) + + pbs1[i+1]/(ubands1[i+1] - lbands1[i+1])*(outubands[i] - lbands1[i+1]) + ) + end + i = length(pbs_combined) + @test pbs_combined[i] ≈ pbs1[i]/(ubands1[i] - lbands1[i])*(ubands1[i] - outlbands[i]) + + end + end + + @testset "non-aligned outbands, multiple input spectrums" begin + for TPB in [ExactProportionalBands{3}, ExactProportionalBands{1}, ExactProportionalBands{12}, + ApproximateThirdOctaveBands, ApproximateOctaveBands] + cbands1 = TPB{:center}(10, 16) + lbands1 = lower_bands(cbands1) + ubands1 = upper_bands(cbands1) + pbs1 = ProportionalBandSpectrum(rand(length(cbands1)), cbands1) + + scaler = cbands1[2]/cbands1[1] + cbands2 = TPB{:center}(10, 15, scaler) + lbands2 = lower_bands(cbands2) + ubands2 = upper_bands(cbands2) + @test all(cbands2 ./ cbands1[1:end-1] .≈ scaler) + pbs2 = ProportionalBandSpectrum(rand(length(cbands2)), cbands2) + + scaler = cbands1[3]/cbands1[1] + cbands3 = TPB{:center}(10, 14, scaler) + lbands3 = lower_bands(cbands3) + ubands3 = upper_bands(cbands3) + @test all(cbands3 ./ cbands1[1:end-2] .≈ scaler) + pbs3 = ProportionalBandSpectrum(rand(length(cbands3)), cbands3) + + scaler = 1.05 + outcbands = TPB{:center}(10, 16, scaler) + outlbands = lower_bands(outcbands) + outubands = upper_bands(outcbands) + @test all(outcbands ./ cbands1 .≈ scaler) + pbs_combined = combine([pbs1, pbs2, pbs3], outcbands) + + i = 1 + @test pbs_combined[i] ≈ ( + pbs1[i]/(ubands1[i] - lbands1[i])*(ubands1[i] - outlbands[i]) + + pbs1[i+1]/(ubands1[i+1] - lbands1[i+1])*(outubands[i] - lbands1[i+1]) + + pbs2[i]/(ubands2[i] - lbands2[i])*(outubands[i] - lbands2[i]) + ) + i = 2 + @test pbs_combined[i] ≈ ( + pbs1[i]/(ubands1[i] - lbands1[i])*(ubands1[i] - outlbands[i]) + + pbs1[i+1]/(ubands1[i+1] - lbands1[i+1])*(outubands[i] - lbands1[i+1]) + + pbs2[i-1]/(ubands2[i-1] - lbands2[i-1])*(ubands2[i-1] - outlbands[i]) + + pbs2[i]/(ubands2[i] - lbands2[i])*(outubands[i] - lbands2[i]) + + pbs3[i-1]/(ubands3[i-1] - lbands3[i-1])*(outubands[i] - lbands3[i-1]) + ) + for i in 3:length(pbs_combined)-1 + if TPB == ApproximateThirdOctaveBands && i == 6 + @test pbs_combined[i] ≈ ( + pbs1[i]/(ubands1[i] - lbands1[i])*(ubands1[i] - outlbands[i]) + + pbs1[i+1]/(ubands1[i+1] - lbands1[i+1])*(outubands[i] - lbands1[i+1]) + + pbs2[i-1]/(ubands2[i-1] - lbands2[i-1])*(ubands2[i-1] - outlbands[i]) + + pbs2[i]/(ubands2[i] - lbands2[i])*(outubands[i] - lbands2[i]) + + pbs3[i-2]/(ubands3[i-2] - lbands3[i-2])*(outubands[i] - outlbands[i]) + ) + else + @test pbs_combined[i] ≈ ( + pbs1[i]/(ubands1[i] - lbands1[i])*(ubands1[i] - outlbands[i]) + + pbs1[i+1]/(ubands1[i+1] - lbands1[i+1])*(outubands[i] - lbands1[i+1]) + + pbs2[i-1]/(ubands2[i-1] - lbands2[i-1])*(ubands2[i-1] - outlbands[i]) + + pbs2[i]/(ubands2[i] - lbands2[i])*(outubands[i] - lbands2[i]) + + pbs3[i-2]/(ubands3[i-2] - lbands3[i-2])*(ubands3[i-2] - outlbands[i]) + + pbs3[i-1]/(ubands3[i-1] - lbands3[i-1])*(outubands[i] - lbands3[i-1]) + ) + end + end + i = length(pbs_combined) + if TPB == ApproximateThirdOctaveBands + @test pbs_combined[i] ≈ ( + pbs1[i]/(ubands1[i] - lbands1[i])*(ubands1[i] - outlbands[i]) + + pbs2[i-1]/(ubands2[i-1] - lbands2[i-1])*(ubands2[i-1] - outlbands[i]) + + pbs3[i-3]/(ubands3[i-3] - lbands3[i-3])*(ubands3[i-3] - outlbands[i]) + + pbs3[i-2] + ) + else + @test pbs_combined[i] ≈ ( + pbs1[i]/(ubands1[i] - lbands1[i])*(ubands1[i] - outlbands[i]) + + pbs2[i-1]/(ubands2[i-1] - lbands2[i-1])*(ubands2[i-1] - outlbands[i]) + + pbs3[i-2]/(ubands3[i-2] - lbands3[i-2])*(ubands3[i-2] - outlbands[i]) + ) + end + end + end + + @testset "non-aligned outbands, multiple input spectrums, all same length" begin + for TPB in [ExactProportionalBands{3}, ExactProportionalBands{1}, ExactProportionalBands{12}, + ApproximateThirdOctaveBands, + ApproximateOctaveBands] + cbands1 = TPB{:center}(10, 16) + lbands1 = lower_bands(cbands1) + ubands1 = upper_bands(cbands1) + pbs1 = ProportionalBandSpectrum(rand(length(cbands1)), cbands1) + + scaler = cbands1[2]/cbands1[1] + cbands2 = TPB{:center}(10, 16, scaler) + lbands2 = lower_bands(cbands2) + ubands2 = upper_bands(cbands2) + pbs2 = ProportionalBandSpectrum(rand(length(cbands2)), cbands2) + + scaler = cbands1[3]/cbands1[1] + cbands3 = TPB{:center}(10, 16, scaler) + lbands3 = lower_bands(cbands3) + ubands3 = upper_bands(cbands3) + pbs3 = ProportionalBandSpectrum(rand(length(cbands3)), cbands3) + + outcbands = TPB{:center}(10, 16, 1.05) + outlbands = lower_bands(outcbands) + outubands = upper_bands(outcbands) + pbs_combined = combine([pbs1, pbs2, pbs3], outcbands) + + i = 1 + @test pbs_combined[i] ≈ ( + pbs1[i]/(ubands1[i] - lbands1[i])*(ubands1[i] - outlbands[i]) + + pbs1[i+1]/(ubands1[i+1] - lbands1[i+1])*(outubands[i] - lbands1[i+1]) + + pbs2[i]/(ubands2[i] - lbands2[i])*(outubands[i] - lbands2[i]) + ) + i = 2 + @test pbs_combined[i] ≈ ( + pbs1[i]/(ubands1[i] - lbands1[i])*(ubands1[i] - outlbands[i]) + + pbs1[i+1]/(ubands1[i+1] - lbands1[i+1])*(outubands[i] - lbands1[i+1]) + + pbs2[i-1]/(ubands2[i-1] - lbands2[i-1])*(ubands2[i-1] - outlbands[i]) + + pbs2[i]/(ubands2[i] - lbands2[i])*(outubands[i] - lbands2[i]) + + pbs3[i-1]/(ubands3[i-1] - lbands3[i-1])*(outubands[i] - lbands3[i-1]) + ) + for i in 3:length(pbs_combined)-1 + if TPB == ApproximateThirdOctaveBands && i == 6 + @test pbs_combined[i] ≈ ( + pbs1[i]/(ubands1[i] - lbands1[i])*(ubands1[i] - outlbands[i]) + + pbs1[i+1]/(ubands1[i+1] - lbands1[i+1])*(outubands[i] - lbands1[i+1]) + + pbs2[i-1]/(ubands2[i-1] - lbands2[i-1])*(ubands2[i-1] - outlbands[i]) + + pbs2[i]/(ubands2[i] - lbands2[i])*(outubands[i] - lbands2[i]) + + pbs3[i-2]/(ubands3[i-2] - lbands3[i-2])*(outubands[i] - outlbands[i]) + ) + else + @test pbs_combined[i] ≈ ( + pbs1[i]/(ubands1[i] - lbands1[i])*(ubands1[i] - outlbands[i]) + + pbs1[i+1]/(ubands1[i+1] - lbands1[i+1])*(outubands[i] - lbands1[i+1]) + + pbs2[i-1]/(ubands2[i-1] - lbands2[i-1])*(ubands2[i-1] - outlbands[i]) + + pbs2[i]/(ubands2[i] - lbands2[i])*(outubands[i] - lbands2[i]) + + pbs3[i-2]/(ubands3[i-2] - lbands3[i-2])*(ubands3[i-2] - outlbands[i]) + + pbs3[i-1]/(ubands3[i-1] - lbands3[i-1])*(outubands[i] - lbands3[i-1]) + ) + end + end + i = length(pbs_combined) + if TPB == ApproximateThirdOctaveBands + @test pbs_combined[i] ≈ ( + pbs1[i]/(ubands1[i] - lbands1[i])*(ubands1[i] - outlbands[i]) + + # pbs1[i+1]/(ubands1[i+1] - lbands1[i+1])*(outubands[i] - lbands1[i+1]) + + pbs2[i-1]/(ubands2[i-1] - lbands2[i-1])*(ubands2[i-1] - outlbands[i]) + + pbs2[i]/(ubands2[i] - lbands2[i])*(outubands[i] - lbands2[i]) + + pbs3[i-3]/(ubands3[i-3] - lbands3[i-3])*(ubands3[i-3] - outlbands[i]) + + pbs3[i-2] + + pbs3[i-1]/(ubands3[i-1] - lbands3[i-1])*(outubands[i] - lbands3[i-1]) + ) + else + @test pbs_combined[i] ≈ ( + pbs1[i]/(ubands1[i] - lbands1[i])*(ubands1[i] - outlbands[i]) + + # pbs1[i+1]/(ubands1[i+1] - lbands1[i+1])*(outubands[i] - lbands1[i+1]) + + pbs2[i-1]/(ubands2[i-1] - lbands2[i-1])*(ubands2[i-1] - outlbands[i]) + + pbs2[i]/(ubands2[i] - lbands2[i])*(outubands[i] - lbands2[i]) + + pbs3[i-2]/(ubands3[i-2] - lbands3[i-2])*(ubands3[i-2] - outlbands[i]) + + pbs3[i-1]/(ubands3[i-1] - lbands3[i-1])*(outubands[i] - lbands3[i-1]) + ) + end + end + end + + @testset "non-aligned wide outbands, multiple input spectrums, all same length" begin + for TPB in [ExactProportionalBands{3}, ExactProportionalBands{1}, ExactProportionalBands{12}, + ApproximateThirdOctaveBands, ApproximateOctaveBands] + cbands1 = TPB{:center}(10, 16) + lbands1 = lower_bands(cbands1) + ubands1 = upper_bands(cbands1) + pbs1 = ProportionalBandSpectrum(rand(length(cbands1)), cbands1) + + scaler = cbands1[2]/cbands1[1] + cbands2 = TPB{:center}(10, 16, scaler) + lbands2 = lower_bands(cbands2) + ubands2 = upper_bands(cbands2) + pbs2 = ProportionalBandSpectrum(rand(length(cbands2)), cbands2) + + scaler = cbands1[3]/cbands1[1] + cbands3 = TPB{:center}(10, 16, scaler) + lbands3 = lower_bands(cbands3) + ubands3 = upper_bands(cbands3) + pbs3 = ProportionalBandSpectrum(rand(length(cbands3)), cbands3) + + outcbands = TPB{:center}(5, 30, 1.05) + outlbands = lower_bands(outcbands) + outubands = upper_bands(outcbands) + pbs_combined = combine([pbs1, pbs2, pbs3], outcbands) + + @test all(pbs_combined[1:4] .≈ 0) + + i = 5 + j = 1 + @test pbs_combined[i] ≈ ( + pbs1[j]/(ubands1[j] - lbands1[j])*(outubands[i] - lbands1[j]) + ) + + i = 6 + j = 1 + @test pbs_combined[i] ≈ ( + pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + ) + i = 7 + j = 2 + @test pbs_combined[i] ≈ ( + pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + ) + for i in 8:11 + j += 1 + if TPB == ApproximateThirdOctaveBands && j == 6 + @test pbs_combined[i] ≈ ( + pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + pbs3[j-2]/(ubands3[j-2] - lbands3[j-2])*(outubands[i] - outlbands[i]) + ) + else + @test pbs_combined[i] ≈ ( + pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + pbs3[j-2]/(ubands3[j-2] - lbands3[j-2])*(ubands3[j-2] - outlbands[i]) + + pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + ) + end + end + i = 12 + j = 7 + if TPB == ApproximateThirdOctaveBands + @test pbs_combined[i] ≈ ( + pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + # pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + pbs3[j-3]/(ubands3[j-3] - lbands3[j-3])*(ubands3[j-3] - outlbands[i]) + + pbs3[j-2] + + pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + ) + else + @test pbs_combined[i] ≈ ( + pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + # pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + pbs3[j-2]/(ubands3[j-2] - lbands3[j-2])*(ubands3[j-2] - outlbands[i]) + + pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + ) + end + i = 13 + j = 8 + @test pbs_combined[i] ≈ ( + # pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + # pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + # pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + pbs3[j-2]/(ubands3[j-2] - lbands3[j-2])*(ubands3[j-2] - outlbands[i]) + + pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + ) + i = 14 + j = 9 + @test pbs_combined[i] ≈ ( + # pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + # pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + # pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + # pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + pbs3[j-2]/(ubands3[j-2] - lbands3[j-2])*(ubands3[j-2] - outlbands[i]) #+ + # pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + ) + @test all(pbs_combined[15:end] .≈ 0) + end + end + + @testset "aligned inbands and outbands, multiple input spectrums, lazy PBS" begin + outcbands = ExactProportionalBands{1}{:center}(5, 30) + outlbands = lower_bands(outcbands) + outubands = upper_bands(outcbands) + + cbands1 = ExactProportionalBands{1}{:center}(11, 16) + lbands1 = lower_bands(cbands1) + ubands1 = upper_bands(cbands1) + # Find a narrowband frequency spacing that will fit in the first outband. + nfreqs_nb_1band = 10 + freq_min_nb_m_half_df_nb_1band = outlbands[band_start(cbands1) - band_start(outcbands) + 1] + freq_max_nb_p_half_df_nb_1band = outubands[band_start(cbands1) - band_start(outcbands) + 1] + df_nb = (freq_max_nb_p_half_df_nb_1band - freq_min_nb_m_half_df_nb_1band)/(nfreqs_nb_1band + 1) + # Now construct a narrowband frequency that spans the bands I'm interested in. + freq_min_nb_m_half_df_nb = outlbands[band_start(cbands1) - band_start(outcbands) + 1] + freq_max_nb_p_half_df_nb = outubands[band_end(cbands1) - band_start(outcbands) + 1] + n = Int(round((freq_max_nb_p_half_df_nb - freq_min_nb_m_half_df_nb)/df_nb)) + 1 + f_lu = range(freq_min_nb_m_half_df_nb, freq_max_nb_p_half_df_nb; length=n) + f_nb = 0.5.*(f_lu[2:end] .+ f_lu[1:end-1]) + @test step(f_nb) ≈ df_nb + @test f_nb[1] > lbands1[1] + @test f_nb[end] < ubands1[end] + @test (f_nb[1] - 0.5*step(f_nb)) ≈ outlbands[band_start(cbands1) - band_start(outcbands) + 1] + @test (f_nb[end] + 0.5*step(f_nb)) ≈ outubands[band_end(cbands1) - band_start(outcbands) + 1] + msp1 = rand(length(f_nb)) + pbs1 = LazyNBProportionalBandSpectrum(f_nb[1], df_nb, msp1, cbands1) + + cbands2 = ExactProportionalBands{1}{:center}(12, 16) + lbands2 = lower_bands(cbands2) + ubands2 = upper_bands(cbands2) + # Find a narrowband frequency spacing that will fit in the second outband. + nfreqs_nb_1band = 10 + freq_min_nb_m_half_df_nb_1band = outlbands[band_start(cbands2) - band_start(outcbands) + 1] + freq_max_nb_p_half_df_nb_1band = outubands[band_start(cbands2) - band_start(outcbands) + 1] + df_nb = (freq_max_nb_p_half_df_nb_1band - freq_min_nb_m_half_df_nb_1band)/(nfreqs_nb_1band + 1) + # Now construct a narrowband frequency that spans the bands I'm interested in. + freq_min_nb_m_half_df_nb = outlbands[band_start(cbands2) - band_start(outcbands) + 1] + freq_max_nb_p_half_df_nb = outubands[band_end(cbands2) - band_start(outcbands) + 1] + n = Int(round((freq_max_nb_p_half_df_nb - freq_min_nb_m_half_df_nb)/df_nb)) + 1 + f_lu = range(freq_min_nb_m_half_df_nb, freq_max_nb_p_half_df_nb; length=n) + f_nb = 0.5.*(f_lu[2:end] .+ f_lu[1:end-1]) + @test step(f_nb) ≈ df_nb + @test f_nb[1] > lbands2[1] + @test f_nb[end] < ubands2[end] + @test (f_nb[1] - 0.5*step(f_nb)) ≈ outlbands[band_start(cbands2) - band_start(outcbands) + 1] + @test (f_nb[end] + 0.5*step(f_nb)) ≈ outubands[band_end(cbands2) - band_start(outcbands) + 1] + msp2 = rand(length(f_nb)) + pbs2 = LazyNBProportionalBandSpectrum(f_nb[1], df_nb, msp2, cbands2) + + cbands3 = ExactProportionalBands{1}{:center}(13, 16) + lbands3 = lower_bands(cbands3) + ubands3 = upper_bands(cbands3) + # Find a narrowband frequency spacing that will fit in the second outband. + nfreqs_nb_1band = 10 + freq_min_nb_m_half_df_nb_1band = outlbands[band_start(cbands3) - band_start(outcbands) + 1] + freq_max_nb_p_half_df_nb_1band = outubands[band_start(cbands3) - band_start(outcbands) + 1] + df_nb = (freq_max_nb_p_half_df_nb_1band - freq_min_nb_m_half_df_nb_1band)/(nfreqs_nb_1band + 1) + # Now construct a narrowband frequency that spans the bands I'm interested in. + freq_min_nb_m_half_df_nb = outlbands[band_start(cbands3) - band_start(outcbands) + 1] + freq_max_nb_p_half_df_nb = outubands[band_end(cbands3) - band_start(outcbands) + 1] + n = Int(round((freq_max_nb_p_half_df_nb - freq_min_nb_m_half_df_nb)/df_nb)) + 1 + f_lu = range(freq_min_nb_m_half_df_nb, freq_max_nb_p_half_df_nb; length=n) + f_nb = 0.5.*(f_lu[2:end] .+ f_lu[1:end-1]) + @test step(f_nb) ≈ df_nb + @test f_nb[1] > lbands3[1] + @test f_nb[end] < ubands3[end] + @test (f_nb[1] - 0.5*step(f_nb)) ≈ outlbands[band_start(cbands3) - band_start(outcbands) + 1] + @test (f_nb[end] + 0.5*step(f_nb)) ≈ outubands[band_end(cbands3) - band_start(outcbands) + 1] + msp3 = rand(length(f_nb)) + pbs3 = LazyNBProportionalBandSpectrum(f_nb[1], df_nb, msp3, cbands3) + + pbs_combined = combine([pbs1, pbs2, pbs3], outcbands) + + f1_nb = frequency_nb(pbs1) + df1_nb = step(f1_nb) + f1_nb_l = f1_nb .- 0.5*df1_nb + f1_nb_u = f1_nb .+ 0.5*df1_nb + + f2_nb = frequency_nb(pbs2) + df2_nb = step(f2_nb) + f2_nb_l = f2_nb .- 0.5*df2_nb + f2_nb_u = f2_nb .+ 0.5*df2_nb + + f3_nb = frequency_nb(pbs3) + df3_nb = step(f3_nb) + f3_nb_l = f3_nb .- 0.5*df3_nb + f3_nb_u = f3_nb .+ 0.5*df3_nb + + for i in 1:length(pbs_combined) + tol = 1e-6 + jstart = searchsortedfirst(f1_nb_l, outlbands[i]-tol) + jend = searchsortedlast(f1_nb_u, outubands[i]+tol) + pbs1_i = sum(msp1[jstart:jend]) + + jstart = searchsortedfirst(f2_nb_l, outlbands[i]-tol) + jend = searchsortedlast(f2_nb_u, outubands[i]+tol) + pbs2_i = sum(msp2[jstart:jend]) + + jstart = searchsortedfirst(f3_nb_l, outlbands[i]-tol) + jend = searchsortedlast(f3_nb_u, outubands[i]+tol) + pbs3_i = sum(msp3[jstart:jend]) + + @test isapprox(pbs_combined[i], pbs1_i + pbs2_i + pbs3_i; atol=1e-12) + end + end + + @testset "non-aligned inbands, aligned outbands, multiple input spectrums, lazy PBS" begin + outcbands = ExactProportionalBands{1}{:center}(5, 30) + outlbands = lower_bands(outcbands) + outubands = upper_bands(outcbands) + + # Find a narrowband frequency spacing that will fit in one of the output bands. + istart = 1 + iend = length(outcbands) + nfreqs_nb_1band = 10 + freq_min_nb_m_half_df_nb_1band = outlbands[istart] + freq_max_nb_p_half_df_nb_1band = outubands[istart] + df_nb = (freq_max_nb_p_half_df_nb_1band - freq_min_nb_m_half_df_nb_1band)/(nfreqs_nb_1band + 1) + # Now construct a narrowband frequency that spans the bands I'm interested in. + freq_min_nb_m_half_df_nb = outlbands[istart] + freq_max_nb_p_half_df_nb = outubands[iend] + n = Int(round((freq_max_nb_p_half_df_nb - freq_min_nb_m_half_df_nb)/df_nb)) + 1 + f_lu = range(freq_min_nb_m_half_df_nb, freq_max_nb_p_half_df_nb; length=n) + f_nb = 0.5.*(f_lu[2:end] .+ f_lu[1:end-1]) + @test step(f_nb) ≈ df_nb + @test (f_nb[1] - 0.5*step(f_nb)) ≈ outlbands[istart] + @test (f_nb[end] + 0.5*step(f_nb)) ≈ outubands[iend] + msp1 = rand(length(f_nb)) + pbs1 = LazyNBProportionalBandSpectrum(ExactProportionalBands{1}, f_nb[1], df_nb, msp1) + + # Find a narrowband frequency spacing that will fit in one of the output bands. + istart = 2 + iend = length(outcbands) - 1 + nfreqs_nb_1band = 10 + freq_min_nb_m_half_df_nb_1band = outlbands[istart] + freq_max_nb_p_half_df_nb_1band = outubands[istart] + df_nb = (freq_max_nb_p_half_df_nb_1band - freq_min_nb_m_half_df_nb_1band)/(nfreqs_nb_1band + 1) + # Now construct a narrowband frequency that spans the bands I'm interested in. + freq_min_nb_m_half_df_nb = outlbands[istart] + freq_max_nb_p_half_df_nb = outubands[iend] + n = Int(round((freq_max_nb_p_half_df_nb - freq_min_nb_m_half_df_nb)/df_nb)) + 1 + f_lu = range(freq_min_nb_m_half_df_nb, freq_max_nb_p_half_df_nb; length=n) + f_nb = 0.5.*(f_lu[2:end] .+ f_lu[1:end-1]) + @test step(f_nb) ≈ df_nb + @test (f_nb[1] - 0.5*step(f_nb)) ≈ outlbands[istart] + @test (f_nb[end] + 0.5*step(f_nb)) ≈ outubands[iend] + msp2 = rand(length(f_nb)) + pbs2 = LazyNBProportionalBandSpectrum(ExactProportionalBands{3}, f_nb[1], df_nb, msp2) + + # Find a narrowband frequency spacing that will fit in one of the output bands. + istart = 3 + iend = length(outcbands) - 2 + nfreqs_nb_1band = 10 + freq_min_nb_m_half_df_nb_1band = outlbands[istart] + freq_max_nb_p_half_df_nb_1band = outubands[istart] + df_nb = (freq_max_nb_p_half_df_nb_1band - freq_min_nb_m_half_df_nb_1band)/(nfreqs_nb_1band + 1) + # Now construct a narrowband frequency that spans the bands I'm interested in. + freq_min_nb_m_half_df_nb = outlbands[istart] + freq_max_nb_p_half_df_nb = outubands[iend] + n = Int(round((freq_max_nb_p_half_df_nb - freq_min_nb_m_half_df_nb)/df_nb)) + 1 + f_lu = range(freq_min_nb_m_half_df_nb, freq_max_nb_p_half_df_nb; length=n) + f_nb = 0.5.*(f_lu[2:end] .+ f_lu[1:end-1]) + @test step(f_nb) ≈ df_nb + @test (f_nb[1] - 0.5*step(f_nb)) ≈ outlbands[istart] + @test (f_nb[end] + 0.5*step(f_nb)) ≈ outubands[iend] + msp3 = rand(length(f_nb)) + pbs3 = LazyNBProportionalBandSpectrum(ExactProportionalBands{12}, f_nb[1], df_nb, msp3) + + pbs_combined = combine([pbs1, pbs2, pbs3], outcbands) + + f1_nb = frequency_nb(pbs1) + df1_nb = step(f1_nb) + f1_nb_l = f1_nb .- 0.5*df1_nb + f1_nb_u = f1_nb .+ 0.5*df1_nb + + f2_nb = frequency_nb(pbs2) + df2_nb = step(f2_nb) + f2_nb_l = f2_nb .- 0.5*df2_nb + f2_nb_u = f2_nb .+ 0.5*df2_nb + + f3_nb = frequency_nb(pbs3) + df3_nb = step(f3_nb) + f3_nb_l = f3_nb .- 0.5*df3_nb + f3_nb_u = f3_nb .+ 0.5*df3_nb + + for i in 1:length(pbs_combined) + tol = 1e-6 + jstart = searchsortedfirst(f1_nb_l, outlbands[i]-tol) + jend = searchsortedlast(f1_nb_u, outubands[i]+tol) + pbs1_i = sum(msp1[jstart:jend]) + + jstart = searchsortedfirst(f2_nb_l, outlbands[i]-tol) + jend = searchsortedlast(f2_nb_u, outubands[i]+tol) + pbs2_i = sum(msp2[jstart:jend]) + + jstart = searchsortedfirst(f3_nb_l, outlbands[i]-tol) + jend = searchsortedlast(f3_nb_u, outubands[i]+tol) + pbs3_i = sum(msp3[jstart:jend]) + + @test isapprox(pbs_combined[i], pbs1_i + pbs2_i + pbs3_i; rtol=1e-12) + end + end + end + + @testset "proportional bands with time" begin + @testset "no time" begin + for TPB in [ExactProportionalBands{3}, ExactProportionalBands{1}, ExactProportionalBands{12}, + ApproximateThirdOctaveBands, + ApproximateOctaveBands] + + cbands1 = TPB{:center}(10, 16) + lbands1 = lower_bands(cbands1) + ubands1 = upper_bands(cbands1) + # Create some random msp corresponding to the proportional bands defined by lbands1, cbands1, ubands1. + nfreq_nb = 800 + freq_min_nb = lbands1[1] + 0.1*(ubands1[1] - lbands1[1]) + freq_max_nb = ubands1[end] - 0.1*(ubands1[end] - lbands1[end]) + df_nb = (freq_max_nb - freq_min_nb)/(nfreq_nb - 1) + @test (freq_min_nb - 0.5*df_nb) > lbands1[1] + @test (freq_max_nb + 0.5*df_nb) < ubands1[end] + f_nb = freq_min_nb .+ (0:(nfreq_nb-1)).*df_nb + msp1 = rand(length(f_nb)) + pbs1 = LazyNBProportionalBandSpectrum(TPB, f_nb[1], df_nb, msp1) + + scaler = cbands1[2]/cbands1[1] + cbands2 = TPB{:center}(10, 16, scaler) + lbands2 = lower_bands(cbands2) + ubands2 = upper_bands(cbands2) + # Create some random msp corresponding to the proportional bands defined by lbands2, cbands2, ubands2. + nfreq_nb = 800 + freq_min_nb = lbands2[1] + 0.1*(ubands2[1] - lbands2[1]) + freq_max_nb = ubands2[end] - 0.1*(ubands2[end] - lbands2[end]) + df_nb = (freq_max_nb - freq_min_nb)/(nfreq_nb - 1) + @test (freq_min_nb - 0.5*df_nb) > lbands2[1] + @test (freq_max_nb + 0.5*df_nb) < ubands2[end] + f_nb = freq_min_nb .+ (0:(nfreq_nb-1)).*df_nb + msp2 = rand(length(f_nb)) + pbs2 = LazyNBProportionalBandSpectrum(TPB, f_nb[1], df_nb, msp2, freq_scaler(cbands2)) + + scaler = cbands1[3]/cbands1[1] + cbands3 = TPB{:center}(10, 16, scaler) + lbands3 = lower_bands(cbands3) + ubands3 = upper_bands(cbands3) + # Create some random msp corresponding to the proportional bands defined by lbands3, cbands3, ubands3. + nfreq_nb = 800 + freq_min_nb = lbands3[1] + 0.1*(ubands3[1] - lbands3[1]) + freq_max_nb = ubands3[end] - 0.1*(ubands3[end] - lbands3[end]) + df_nb = (freq_max_nb - freq_min_nb)/(nfreq_nb - 1) + @test (freq_min_nb - 0.5*df_nb) > lbands3[1] + @test (freq_max_nb + 0.5*df_nb) < ubands3[end] + f_nb = freq_min_nb .+ (0:(nfreq_nb-1)).*df_nb + msp3 = rand(length(f_nb)) + pbs3 = LazyNBProportionalBandSpectrum(TPB, f_nb[1], df_nb, msp3, freq_scaler(cbands3)) + + T = time_period([pbs1, pbs2, pbs3]) + @test T ≈ -Inf + @test time_scaler(pbs1, T) ≈ 1 + @test time_scaler(pbs2, T) ≈ 1 + @test time_scaler(pbs3, T) ≈ 1 + + end + + end + + @testset "with time" begin + @testset "all same time step" begin + + for TPB in [ExactProportionalBands{3}, ExactProportionalBands{1}, ExactProportionalBands{12}, + ApproximateThirdOctaveBands, ApproximateOctaveBands] + cbands1 = TPB{:center}(10, 16) + lbands1 = lower_bands(cbands1) + ubands1 = upper_bands(cbands1) + t1 = 2.0 + dt1 = 0.2 + pbs1 = ProportionalBandSpectrumWithTime(rand(length(cbands1)), cbands1, dt1, t1) + @test has_observer_time(pbs1) == true + @test observer_time(pbs1) ≈ t1 + @test timestep(pbs1) ≈ dt1 + + scaler = cbands1[2]/cbands1[1] + cbands2 = TPB{:center}(10, 16, scaler) + lbands2 = lower_bands(cbands2) + ubands2 = upper_bands(cbands2) + t2 = 2.1 + dt2 = 0.2 + pbs2 = ProportionalBandSpectrumWithTime(rand(length(cbands2)), cbands2, dt2, t2) + @test has_observer_time(pbs2) == true + @test observer_time(pbs2) ≈ t2 + @test timestep(pbs2) ≈ dt2 + + scaler = cbands1[3]/cbands1[1] + cbands3 = TPB{:center}(10, 16, scaler) + lbands3 = lower_bands(cbands3) + ubands3 = upper_bands(cbands3) + t3 = 2.3 + dt3 = 0.2 + pbs3 = ProportionalBandSpectrumWithTime(rand(length(cbands3)), cbands3, dt3, t3) + @test has_observer_time(pbs3) == true + @test observer_time(pbs3) ≈ t3 + @test timestep(pbs3) ≈ dt3 + + T = time_period([pbs1, pbs2, pbs3]) + @test T ≈ t3 - t1 + tscaler1 = dt1/T + tscaler2 = dt2/T + tscaler3 = dt3/T + @test time_scaler(pbs1, T) ≈ tscaler1 + @test time_scaler(pbs2, T) ≈ tscaler2 + @test time_scaler(pbs3, T) ≈ tscaler3 + + outcbands = TPB{:center}(5, 30, 1.05) + outlbands = lower_bands(outcbands) + outubands = upper_bands(outcbands) + pbs_combined = combine([pbs1, pbs2, pbs3], outcbands) + + @test all(pbs_combined[1:4] .≈ 0) + + i = 5 + j = 1 + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(outubands[i] - lbands1[j]) + ) + + i = 6 + j = 1 + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + ) + i = 7 + j = 2 + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + ) + for i in 8:11 + j += 1 + if TPB == ApproximateThirdOctaveBands && j == 6 + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-2]/(ubands3[j-2] - lbands3[j-2])*(outubands[i] - outlbands[i]) + ) + else + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-2]/(ubands3[j-2] - lbands3[j-2])*(ubands3[j-2] - outlbands[i]) + + tscaler3*pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + ) + end + end + i = 12 + j = 7 + if TPB == ApproximateThirdOctaveBands + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + # tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-3]/(ubands3[j-3] - lbands3[j-3])*(ubands3[j-3] - outlbands[i]) + + tscaler3*pbs3[j-2] + + tscaler3*pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + ) + else + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + # tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-2]/(ubands3[j-2] - lbands3[j-2])*(ubands3[j-2] - outlbands[i]) + + tscaler3*pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + ) + end + i = 13 + j = 8 + @test pbs_combined[i] ≈ ( + # tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + # tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + # tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-2]/(ubands3[j-2] - lbands3[j-2])*(ubands3[j-2] - outlbands[i]) + + tscaler3*pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + ) + i = 14 + j = 9 + @test pbs_combined[i] ≈ ( + # tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + # tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + # tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + # tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-2]/(ubands3[j-2] - lbands3[j-2])*(ubands3[j-2] - outlbands[i]) #+ + # tscaler3*pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + ) + @test all(pbs_combined[15:end] .≈ 0) + end + end + + @testset "different time steps" begin + for TPB in [ExactProportionalBands{3}, ExactProportionalBands{1}, ExactProportionalBands{12}, + ApproximateThirdOctaveBands, ApproximateOctaveBands] + cbands1 = TPB{:center}(10, 16) + lbands1 = lower_bands(cbands1) + ubands1 = upper_bands(cbands1) + t1 = 2.0 + dt1 = 0.2 + pbs1 = ProportionalBandSpectrumWithTime(rand(length(cbands1)), cbands1, dt1, t1) + @test has_observer_time(pbs1) == true + @test observer_time(pbs1) ≈ t1 + + scaler = cbands1[2]/cbands1[1] + cbands2 = TPB{:center}(10, 16, scaler) + lbands2 = lower_bands(cbands2) + ubands2 = upper_bands(cbands2) + t2 = 2.1 + dt2 = 0.3 + pbs2 = ProportionalBandSpectrumWithTime(rand(length(cbands2)), cbands2, dt2, t2) + @test has_observer_time(pbs2) == true + @test observer_time(pbs2) ≈ t2 + + scaler = cbands1[3]/cbands1[1] + cbands3 = TPB{:center}(10, 16, scaler) + lbands3 = lower_bands(cbands3) + ubands3 = upper_bands(cbands3) + t3 = 2.3 + dt3 = 0.4 + pbs3 = ProportionalBandSpectrumWithTime(rand(length(cbands3)), cbands3, dt3, t3) + @test has_observer_time(pbs3) == true + @test observer_time(pbs3) ≈ t3 + + T = time_period([pbs1, pbs2, pbs3]) + @test T ≈ t3 - t1 + tscaler1 = dt1/T + tscaler2 = dt2/T + tscaler3 = dt3/T + @test time_scaler(pbs1, T) ≈ tscaler1 + @test time_scaler(pbs2, T) ≈ tscaler2 + @test time_scaler(pbs3, T) ≈ tscaler3 + + outcbands = TPB{:center}(5, 30, 1.05) + outlbands = lower_bands(outcbands) + outubands = upper_bands(outcbands) + pbs_combined = combine([pbs1, pbs2, pbs3], outcbands) + + @test all(pbs_combined[1:4] .≈ 0) + + i = 5 + j = 1 + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(outubands[i] - lbands1[j]) + ) + + i = 6 + j = 1 + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + ) + i = 7 + j = 2 + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + ) + for i in 8:11 + j += 1 + if TPB == ApproximateThirdOctaveBands && j == 6 + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-2]/(ubands3[j-2] - lbands3[j-2])*(outubands[i] - outlbands[i]) + ) + else + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-2]/(ubands3[j-2] - lbands3[j-2])*(ubands3[j-2] - outlbands[i]) + + tscaler3*pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + ) + end + end + i = 12 + j = 7 + if TPB == ApproximateThirdOctaveBands + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + # tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-3]/(ubands3[j-3] - lbands3[j-3])*(ubands3[j-3] - outlbands[i]) + + tscaler3*pbs3[j-2] + + tscaler3*pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + ) + else + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + # tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-2]/(ubands3[j-2] - lbands3[j-2])*(ubands3[j-2] - outlbands[i]) + + tscaler3*pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + ) + end + i = 13 + j = 8 + @test pbs_combined[i] ≈ ( + # tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + # tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + # tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-2]/(ubands3[j-2] - lbands3[j-2])*(ubands3[j-2] - outlbands[i]) + + tscaler3*pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + ) + i = 14 + j = 9 + @test pbs_combined[i] ≈ ( + # tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + # tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + # tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + # tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-2]/(ubands3[j-2] - lbands3[j-2])*(ubands3[j-2] - outlbands[i]) #+ + # tscaler3*pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + ) + @test all(pbs_combined[15:end] .≈ 0) + end + end + + @testset "mix of with and without time" begin + for TPB in [ExactProportionalBands{3}, ExactProportionalBands{1}, ExactProportionalBands{12}, + ApproximateThirdOctaveBands, ApproximateOctaveBands] + cbands1 = TPB{:center}(10, 16) + lbands1 = lower_bands(cbands1) + ubands1 = upper_bands(cbands1) + t1 = 2.0 + dt1 = 0.2 + pbs1 = ProportionalBandSpectrumWithTime(rand(length(cbands1)), cbands1, dt1, t1) + @test has_observer_time(pbs1) == true + @test observer_time(pbs1) ≈ t1 + + scaler = cbands1[2]/cbands1[1] + cbands2 = TPB{:center}(10, 16, scaler) + lbands2 = lower_bands(cbands2) + ubands2 = upper_bands(cbands2) + # t2 = 2.1 + # dt2 = 0.3 + pbs2 = ProportionalBandSpectrum(rand(length(cbands2)), cbands2) + @test has_observer_time(pbs2) == false + @test observer_time(pbs2) ≈ 0 + + scaler = cbands1[3]/cbands1[1] + cbands3 = TPB{:center}(10, 16, scaler) + lbands3 = lower_bands(cbands3) + ubands3 = upper_bands(cbands3) + t3 = 2.3 + dt3 = 0.4 + pbs3 = ProportionalBandSpectrumWithTime(rand(length(cbands3)), cbands3, dt3, t3) + @test has_observer_time(pbs3) == true + @test observer_time(pbs3) ≈ t3 + + T = time_period([pbs1, pbs2, pbs3]) + @test T ≈ t3 - t1 + tscaler1 = dt1/T + tscaler2 = 1.0 + tscaler3 = dt3/T + @test time_scaler(pbs1, T) ≈ tscaler1 + @test time_scaler(pbs2, T) ≈ tscaler2 + @test time_scaler(pbs3, T) ≈ tscaler3 + + outcbands = TPB{:center}(5, 30, 1.05) + outlbands = lower_bands(outcbands) + outubands = upper_bands(outcbands) + pbs_combined = combine([pbs1, pbs2, pbs3], outcbands) + + @test all(pbs_combined[1:4] .≈ 0) + + i = 5 + j = 1 + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(outubands[i] - lbands1[j]) + ) + + i = 6 + j = 1 + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + ) + i = 7 + j = 2 + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + ) + for i in 8:11 + j += 1 + if TPB == ApproximateThirdOctaveBands && j == 6 + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-2]/(ubands3[j-2] - lbands3[j-2])*(outubands[i] - outlbands[i]) + ) + else + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-2]/(ubands3[j-2] - lbands3[j-2])*(ubands3[j-2] - outlbands[i]) + + tscaler3*pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + ) + end + end + i = 12 + j = 7 + if TPB == ApproximateThirdOctaveBands + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + # tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-3]/(ubands3[j-3] - lbands3[j-3])*(ubands3[j-3] - outlbands[i]) + + tscaler3*pbs3[j-2] + + tscaler3*pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + ) + else + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + # tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-2]/(ubands3[j-2] - lbands3[j-2])*(ubands3[j-2] - outlbands[i]) + + tscaler3*pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + ) + end + i = 13 + j = 8 + @test pbs_combined[i] ≈ ( + # tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + # tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + # tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-2]/(ubands3[j-2] - lbands3[j-2])*(ubands3[j-2] - outlbands[i]) + + tscaler3*pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + ) + i = 14 + j = 9 + @test pbs_combined[i] ≈ ( + # tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + # tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + # tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + # tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-2]/(ubands3[j-2] - lbands3[j-2])*(ubands3[j-2] - outlbands[i]) #+ + # tscaler3*pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + ) + @test all(pbs_combined[15:end] .≈ 0) + end + end + + @testset "different time steps, two dimensional arrays" begin + for TPB in [ExactProportionalBands{3}, ExactProportionalBands{1}, ExactProportionalBands{12}, + ApproximateThirdOctaveBands, ApproximateOctaveBands] + cbands1 = TPB{:center}(10, 16) + lbands1 = lower_bands(cbands1) + ubands1 = upper_bands(cbands1) + t1 = 2.0 + dt1 = 0.2 + pbs1 = ProportionalBandSpectrumWithTime(rand(length(cbands1)), cbands1, dt1, t1) + @test has_observer_time(pbs1) == true + @test observer_time(pbs1) ≈ t1 + + scaler = cbands1[2]/cbands1[1] + cbands2 = TPB{:center}(10, 16, scaler) + lbands2 = lower_bands(cbands2) + ubands2 = upper_bands(cbands2) + t2 = 2.1 + dt2 = 0.3 + pbs2 = ProportionalBandSpectrumWithTime(rand(length(cbands2)), cbands2, dt2, t2) + @test has_observer_time(pbs2) == true + @test observer_time(pbs2) ≈ t2 + + scaler = cbands1[3]/cbands1[1] + cbands3 = TPB{:center}(10, 16, scaler) + lbands3 = lower_bands(cbands3) + ubands3 = upper_bands(cbands3) + t3 = 2.3 + dt3 = 0.4 + pbs3 = ProportionalBandSpectrumWithTime(rand(length(cbands3)), cbands3, dt3, t3) + @test has_observer_time(pbs3) == true + @test observer_time(pbs3) ≈ t3 + + cbands4 = TPB{:center}(10, 16) + lbands4 = lower_bands(cbands1) + ubands4 = upper_bands(cbands1) + t4 = 2.0 + dt4 = 0.2 + pbs4 = ProportionalBandSpectrumWithTime(rand(length(cbands4)), cbands4, dt4, t4) + @test has_observer_time(pbs4) == true + @test observer_time(pbs4) ≈ t4 + + scaler = cbands4[2]/cbands4[1] + cbands5 = TPB{:center}(10, 16, scaler) + lbands5 = lower_bands(cbands5) + ubands5 = upper_bands(cbands5) + t5 = 5.3 + dt5 = 0.6 + pbs5 = ProportionalBandSpectrumWithTime(rand(length(cbands5)), cbands5, dt5, t5) + @test has_observer_time(pbs5) == true + @test observer_time(pbs5) ≈ t5 + + scaler = cbands4[3]/cbands4[1] + cbands6 = TPB{:center}(10, 16, scaler) + lbands6 = lower_bands(cbands6) + ubands6 = upper_bands(cbands6) + t6 = 2.1 + dt6 = 0.7 + pbs6 = ProportionalBandSpectrumWithTime(rand(length(cbands6)), cbands6, dt6, t6) + @test has_observer_time(pbs6) == true + @test observer_time(pbs6) ≈ t6 + + pbss = hcat([pbs1, pbs2, pbs3], [pbs4, pbs5, pbs6]) + @test size(pbss) == (3, 2) + # time period for column 1. + Tc1 = time_period(pbss[:, 1]) + @test Tc1 ≈ t3 - t1 + # time period for column 2. + Tc2 = time_period(pbss[:, 2]) + @test Tc2 ≈ t5 - t4 + tscaler1 = dt1/Tc1 + tscaler2 = dt2/Tc1 + tscaler3 = dt3/Tc1 + tscaler4 = dt4/Tc2 + tscaler5 = dt5/Tc2 + tscaler6 = dt6/Tc2 + @test time_scaler(pbs1, Tc1) ≈ tscaler1 + @test time_scaler(pbs2, Tc1) ≈ tscaler2 + @test time_scaler(pbs3, Tc1) ≈ tscaler3 + @test time_scaler(pbs4, Tc2) ≈ tscaler4 + @test time_scaler(pbs5, Tc2) ≈ tscaler5 + @test time_scaler(pbs6, Tc2) ≈ tscaler6 + + outcbands = TPB{:center}(5, 30, 1.05) + outlbands = lower_bands(outcbands) + outubands = upper_bands(outcbands) + pbs_combined = combine(pbss, outcbands) + + @test all(pbs_combined[1:4] .≈ 0) + + i = 5 + j = 1 + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(outubands[i] - lbands1[j]) + + tscaler4*pbs4[j]/(ubands4[j] - lbands4[j])*(outubands[i] - lbands4[j]) + ) + + i = 6 + j = 1 + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler4*pbs4[j]/(ubands4[j] - lbands4[j])*(ubands4[j] - outlbands[i]) + + tscaler4*pbs4[j+1]/(ubands4[j+1] - lbands4[j+1])*(outubands[i] - lbands4[j+1]) + + tscaler5*pbs5[j]/(ubands5[j] - lbands5[j])*(outubands[i] - lbands5[j]) + ) + i = 7 + j = 2 + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + + tscaler4*pbs4[j]/(ubands4[j] - lbands4[j])*(ubands4[j] - outlbands[i]) + + tscaler4*pbs4[j+1]/(ubands4[j+1] - lbands4[j+1])*(outubands[i] - lbands4[j+1]) + + tscaler5*pbs5[j-1]/(ubands5[j-1] - lbands5[j-1])*(ubands5[j-1] - outlbands[i]) + + tscaler5*pbs5[j]/(ubands5[j] - lbands5[j])*(outubands[i] - lbands5[j]) + + tscaler6*pbs6[j-1]/(ubands6[j-1] - lbands6[j-1])*(outubands[i] - lbands6[j-1]) + ) + for i in 8:11 + j += 1 + if TPB == ApproximateThirdOctaveBands && j == 6 + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-2]/(ubands3[j-2] - lbands3[j-2])*(outubands[i] - outlbands[i]) + + tscaler4*pbs4[j]/(ubands4[j] - lbands4[j])*(ubands4[j] - outlbands[i]) + + tscaler4*pbs4[j+1]/(ubands4[j+1] - lbands4[j+1])*(outubands[i] - lbands4[j+1]) + + tscaler5*pbs5[j-1]/(ubands5[j-1] - lbands5[j-1])*(ubands5[j-1] - outlbands[i]) + + tscaler5*pbs5[j]/(ubands5[j] - lbands5[j])*(outubands[i] - lbands5[j]) + + tscaler6*pbs6[j-2]/(ubands6[j-2] - lbands6[j-2])*(outubands[i] - outlbands[i]) + ) + else + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-2]/(ubands3[j-2] - lbands3[j-2])*(ubands3[j-2] - outlbands[i]) + + tscaler3*pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + + tscaler4*pbs4[j]/(ubands4[j] - lbands4[j])*(ubands4[j] - outlbands[i]) + + tscaler4*pbs4[j+1]/(ubands4[j+1] - lbands4[j+1])*(outubands[i] - lbands4[j+1]) + + tscaler5*pbs5[j-1]/(ubands5[j-1] - lbands5[j-1])*(ubands5[j-1] - outlbands[i]) + + tscaler5*pbs5[j]/(ubands5[j] - lbands5[j])*(outubands[i] - lbands5[j]) + + tscaler6*pbs6[j-2]/(ubands6[j-2] - lbands6[j-2])*(ubands6[j-2] - outlbands[i]) + + tscaler6*pbs6[j-1]/(ubands6[j-1] - lbands6[j-1])*(outubands[i] - lbands6[j-1]) + ) + end + end + i = 12 + j = 7 + if TPB == ApproximateThirdOctaveBands + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + # tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-3]/(ubands3[j-3] - lbands3[j-3])*(ubands3[j-3] - outlbands[i]) + + tscaler3*pbs3[j-2] + + tscaler3*pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + + tscaler4*pbs4[j]/(ubands4[j] - lbands4[j])*(ubands4[j] - outlbands[i]) + + # tscaler4*pbs4[j+1]/(ubands4[j+1] - lbands4[j+1])*(outubands[i] - lbands4[j+1]) + + tscaler5*pbs5[j-1]/(ubands5[j-1] - lbands5[j-1])*(ubands5[j-1] - outlbands[i]) + + tscaler5*pbs5[j]/(ubands5[j] - lbands5[j])*(outubands[i] - lbands5[j]) + + tscaler6*pbs6[j-3]/(ubands6[j-3] - lbands6[j-3])*(ubands6[j-3] - outlbands[i]) + + tscaler6*pbs6[j-2] + + tscaler6*pbs6[j-1]/(ubands6[j-1] - lbands6[j-1])*(outubands[i] - lbands6[j-1]) + ) + else + @test pbs_combined[i] ≈ ( + tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + # tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-2]/(ubands3[j-2] - lbands3[j-2])*(ubands3[j-2] - outlbands[i]) + + tscaler3*pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + + tscaler4*pbs4[j]/(ubands4[j] - lbands4[j])*(ubands4[j] - outlbands[i]) + + # tscaler4*pbs4[j+1]/(ubands4[j+1] - lbands4[j+1])*(outubands[i] - lbands4[j+1]) + + tscaler5*pbs5[j-1]/(ubands5[j-1] - lbands5[j-1])*(ubands5[j-1] - outlbands[i]) + + tscaler5*pbs5[j]/(ubands5[j] - lbands5[j])*(outubands[i] - lbands5[j]) + + tscaler6*pbs6[j-2]/(ubands6[j-2] - lbands6[j-2])*(ubands6[j-2] - outlbands[i]) + + tscaler6*pbs6[j-1]/(ubands6[j-1] - lbands6[j-1])*(outubands[i] - lbands6[j-1]) + ) + end + i = 13 + j = 8 + @test pbs_combined[i] ≈ ( + # tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + # tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + # tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-2]/(ubands3[j-2] - lbands3[j-2])*(ubands3[j-2] - outlbands[i]) + + tscaler3*pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + + # tscaler4*pbs4[j]/(ubands4[j] - lbands4[j])*(ubands4[j] - outlbands[i]) + + # tscaler4*pbs4[j+1]/(ubands4[j+1] - lbands4[j+1])*(outubands[i] - lbands4[j+1]) + + tscaler5*pbs5[j-1]/(ubands5[j-1] - lbands5[j-1])*(ubands5[j-1] - outlbands[i]) + + # tscaler5*pbs5[j]/(ubands5[j] - lbands5[j])*(outubands[i] - lbands5[j]) + + tscaler6*pbs6[j-2]/(ubands6[j-2] - lbands6[j-2])*(ubands6[j-2] - outlbands[i]) + + tscaler6*pbs6[j-1]/(ubands6[j-1] - lbands6[j-1])*(outubands[i] - lbands6[j-1]) + ) + i = 14 + j = 9 + @test pbs_combined[i] ≈ ( + # tscaler1*pbs1[j]/(ubands1[j] - lbands1[j])*(ubands1[j] - outlbands[i]) + + # tscaler1*pbs1[j+1]/(ubands1[j+1] - lbands1[j+1])*(outubands[i] - lbands1[j+1]) + + # tscaler2*pbs2[j-1]/(ubands2[j-1] - lbands2[j-1])*(ubands2[j-1] - outlbands[i]) + + # tscaler2*pbs2[j]/(ubands2[j] - lbands2[j])*(outubands[i] - lbands2[j]) + + tscaler3*pbs3[j-2]/(ubands3[j-2] - lbands3[j-2])*(ubands3[j-2] - outlbands[i]) + + # tscaler3*pbs3[j-1]/(ubands3[j-1] - lbands3[j-1])*(outubands[i] - lbands3[j-1]) + + # tscaler4*pbs4[j]/(ubands4[j] - lbands4[j])*(ubands4[j] - outlbands[i]) + + # tscaler4*pbs4[j+1]/(ubands4[j+1] - lbands4[j+1])*(outubands[i] - lbands4[j+1]) + + # tscaler5*pbs5[j-1]/(ubands5[j-1] - lbands5[j-1])*(ubands5[j-1] - outlbands[i]) + + # tscaler5*pbs5[j]/(ubands5[j] - lbands5[j])*(outubands[i] - lbands5[j]) + + tscaler6*pbs6[j-2]/(ubands6[j-2] - lbands6[j-2])*(ubands6[j-2] - outlbands[i]) #+ + # tscaler6*pbs6[j-1]/(ubands6[j-1] - lbands6[j-1])*(outubands[i] - lbands6[j-1]) + ) + @test all(pbs_combined[15:end] .≈ 0) + + # Transpose to switch the time axis and compare to the original. + time_axis = 2 + pbss_t = permutedims(pbss, (2, 1)) + pbs_combined_t = combine(pbss_t, outcbands, time_axis) + @test all(pbs_combined_t .≈ pbs_combined) + end + end + end end end