Skip to content

Commit

Permalink
expands read/write capabilities of SampleStreams
Browse files Browse the repository at this point in the history
  • Loading branch information
ssfrr committed May 13, 2017
1 parent 69fb8a3 commit c0ea736
Show file tree
Hide file tree
Showing 5 changed files with 206 additions and 140 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ write(wrapper, source)

The `ResampleSink` wrapper type wraps around a sink. Writing to this wrapper sink will resample the given data and pass it to the original sink. It maintains state between writes so that the interpolation is correct across the boundaries of multiple writes.

Currently `ResampleSink` handles resampling with simple linear interpolation and no lowpass filtering when downsampling. In the future we will likely implement other resampling methods.
`ResampleSink` handles resampling with polyphase FIR resampling filter.

### Channel Conversion

Expand Down
88 changes: 68 additions & 20 deletions src/SampleStream.jl
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function unsafe_write end
blocksize(src::SampleSource) = 0
blocksize(src::SampleSink) = 0

toindex(stream::SampleSource, t::SecondsQuantity) = round(Int, float(t)*compat_samplerate(stream)) + 1
toindex(stream::SampleSource, t::SecondsQuantity) = round(Int, float(t)*samplerate(stream)) + 1

# subtypes should only have to implement the `unsafe_read!` and `unsafe_write` methods, so
# here we implement all the converting wrapper methods
Expand All @@ -60,7 +60,7 @@ toindex(stream::SampleSource, t::SecondsQuantity) = round(Int, float(t)*compat_s
Base.read(stream::SampleSource, t::SecondsQuantity) = read(stream, toindex(stream, t)-1)

function Base.read(src::SampleSource, nframes::Integer)
buf = SampleBuf(eltype(src), compat_samplerate(src), nframes, nchannels(src))
buf = SampleBuf(eltype(src), samplerate(src), nframes, nchannels(src))
n = read!(src, buf)

buf[1:n, :]
Expand All @@ -71,7 +71,7 @@ const DEFAULT_BLOCKSIZE=4096
# handle sink-to-source writing with a duration in seconds
function Base.write(sink::SampleSink, source::SampleSource,
duration::SecondsQuantity; blocksize=-1)
sr = compat_samplerate(sink)
sr = samplerate(sink)
frames = trunc(Int, float(duration) * sr)
n = write(sink, source, frames; blocksize=blocksize)

Expand All @@ -86,18 +86,18 @@ end
# TODO: we should be able to add reformatting support to the ResampleSink and
# xMixSink types, to avoid an extra buffer copy
function wrap_sink(sink::SampleSink, source::SampleSource, blocksize)
if eltype(sink) != eltype(source) && !isapprox(compat_samplerate(sink), compat_samplerate(source))
if eltype(sink) != eltype(source) && !isapprox(samplerate(sink), samplerate(source))
# we're going to resample AND reformat. We prefer to resample
# in the floating-point space because it seems to be about 40% faster
if eltype(sink) <: AbstractFloat
wrap_sink(ResampleSink(sink, compat_samplerate(source), blocksize), source, blocksize)
wrap_sink(ResampleSink(sink, samplerate(source), blocksize), source, blocksize)
else
wrap_sink(ReformatSink(sink, eltype(source), blocksize), source, blocksize)
end
elseif eltype(sink) != eltype(source)
wrap_sink(ReformatSink(sink, eltype(source), blocksize), source, blocksize)
elseif !isapprox(compat_samplerate(sink), compat_samplerate(source))
wrap_sink(ResampleSink(sink, compat_samplerate(source), blocksize), source, blocksize)
elseif !isapprox(samplerate(sink), samplerate(source))
wrap_sink(ResampleSink(sink, samplerate(source), blocksize), source, blocksize)
elseif nchannels(sink) != nchannels(source)
if nchannels(sink) == 1
DownMixSink(sink, nchannels(source), blocksize)
Expand Down Expand Up @@ -147,7 +147,7 @@ end
function Base.write(sink::SampleSink, buf::SampleBuf, nframes=nframes(buf))
if nchannels(sink) == nchannels(buf) &&
eltype(sink) == eltype(buf) &&
isapprox(compat_samplerate(sink), compat_samplerate(buf))
isapprox(samplerate(sink), samplerate(buf))
# everything matches, call the sink's low-level write method
unsafe_write(sink, buf.data, 0, nframes)
else
Expand All @@ -158,7 +158,13 @@ function Base.write(sink::SampleSink, buf::SampleBuf, nframes=nframes(buf))
end

function Base.write(sink::SampleSink, buf::SampleBuf, duration::SecondsQuantity)
write(sink, buf, round(Int, float(duration)*samplerate(buf)))
n = round(Int, float(duration)*samplerate(buf))
written = write(sink, buf, n)
if written == n
return duration
else
return written / samplerate(buf) * s
end
end

# treat bare arrays as a buffer with the same samplerate as the sink
Expand All @@ -167,25 +173,67 @@ function Base.write(sink::SampleSink, arr::Array, dur=nframes(arr))
write(sink, buf, dur)
end

# TODO: it seems like read! should support a duration
function Base.read!(source::SampleSource, buf::SampleBuf)
function Base.read!(source::SampleSource, buf::SampleBuf, n::Integer)
if nchannels(source) == nchannels(buf) &&
eltype(source) == eltype(buf) &&
isapprox(compat_samplerate(source), compat_samplerate(buf))
unsafe_read!(source, buf.data, 0, nframes(buf))
isapprox(samplerate(source), samplerate(buf))
unsafe_read!(source, buf.data, 0, n)
else
# some conversion is necessary. Wrap in a sink so we can use the
# stream conversion machinery
write(SampleBufSink(buf), source)
write(SampleBufSink(buf), source, n)
end
end

# when reading into a SampleBuf, calculate frames based on the given buffer,
# which might differ from the source samplerate if there's a samplerate
# conversion involved.
function Base.read!(source::SampleSource, buf::SampleBuf, t::SecondsQuantity)
n = round(Int, float(t)*samplerate(buf))
written = read!(source, buf, n)
if written == n
return t
else
return written / samplerate(buf) * s
end
end

function Base.read!(source::SampleSource, buf::Array, t::SecondsQuantity)
n = round(Int, float(t)*samplerate(source))
written = read!(source, buf, n)
if written == n
return t
else
return written / samplerate(buf) * s
end
end

# treat bare arrays as a buffer with the same samplerate as the source
function Base.read!(source::SampleSource, arr::Array)
function Base.read!(source::SampleSource, arr::Array, n::Integer)
buf = SampleBuf(arr, samplerate(source))
read!(source, buf)
read!(source, buf, n)
end

# if no frame count is given default to the number of frames in the destination
Base.read!(source::SampleSource, arr::AbstractArray) = read!(source, arr, nframes(arr))

function Base.read(source::SampleSource)
buf = SampleBuf(eltype(source),
samplerate(source),
DEFAULT_BLOCKSIZE,
nchannels(source))
# during accumulation we keep the channels separate so we can grow the
# arrays without needing to copy data around as much
cumbufs = [Vector{eltype(source)}() for _ in 1:nchannels(source)]
while true
n = read!(source, buf)
for ch in 1:length(cumbufs)
append!(cumbufs[ch], @view buf.data[1:n, ch])
end
n == nframes(buf) || break
end
SampleBuf(hcat(cumbufs...), samplerate(source))
end

"""UpMixSink provides a single-channel sink that wraps a multi-channel sink.
Writing to this sink copies the single channel to all the channels in the
Expand All @@ -203,7 +251,7 @@ function UpMixSink(wrapped::SampleSink, blocksize=DEFAULT_BLOCKSIZE)
UpMixSink(wrapped, buf)
end

samplerate(sink::UpMixSink) = compat_samplerate(sink.wrapped)
samplerate(sink::UpMixSink) = samplerate(sink.wrapped)
nchannels(sink::UpMixSink) = 1
Base.eltype(sink::UpMixSink) = eltype(sink.wrapped)
blocksize(sink::UpMixSink) = size(sink.buf, 1)
Expand Down Expand Up @@ -243,7 +291,7 @@ function DownMixSink(wrapped::SampleSink, channels, blocksize=DEFAULT_BLOCKSIZE)
DownMixSink(wrapped, buf, channels)
end

samplerate(sink::DownMixSink) = compat_samplerate(sink.wrapped)
samplerate(sink::DownMixSink) = samplerate(sink.wrapped)
nchannels(sink::DownMixSink) = sink.channels
Base.eltype(sink::DownMixSink) = eltype(sink.wrapped)
blocksize(sink::DownMixSink) = size(sink.buf, 1)
Expand Down Expand Up @@ -287,7 +335,7 @@ function ReformatSink(wrapped::SampleSink, T, blocksize=DEFAULT_BLOCKSIZE)
ReformatSink(wrapped, buf, T)
end

samplerate(sink::ReformatSink) = compat_samplerate(sink.wrapped)
samplerate(sink::ReformatSink) = samplerate(sink.wrapped)
nchannels(sink::ReformatSink) = nchannels(sink.wrapped)
Base.eltype(sink::ReformatSink) = sink.typ
blocksize(sink::ReformatSink) = nframes(sink.buf)
Expand Down Expand Up @@ -320,7 +368,7 @@ type ResampleSink{W <: SampleSink, B <: Array, F <: FIRFilter} <: SampleSink
end

function ResampleSink(wrapped::SampleSink, sr, blocksize=DEFAULT_BLOCKSIZE)
wsr = compat_samplerate(wrapped)
wsr = samplerate(wrapped)
T = eltype(wrapped)
N = nchannels(wrapped)
buf = Array{T}(blocksize, N)
Expand Down
97 changes: 0 additions & 97 deletions src/deprecated.jl
Original file line number Diff line number Diff line change
@@ -1,97 +0,0 @@
function unsafe_read! end
function unsafe_write end

# apparently stacktrace doesn't exist on 0.4, so too bad, 0.4 users don't get
# stack traces on their deprecation warnings
if !isdefined(:stacktrace)
stacktrace() = []
end

# fallback function for libraries using the old unsafe_read! function
const read_depwarned = Set{Type}()
function unsafe_read!{T <: SampleSource}(src::T, buf::Array, frameoffset, framecount)
if !(T in read_depwarned)
push!(read_depwarned, T)
warn("""`unsafe_read!(src::$T, buf::Array, frameoffset, framecount)` not defined,
falling back to deprecated `unsafe_read!(src::$T, buf::SampleBuf)`. Please
check the SampledSignals README for the new API""")
map(println, stacktrace())
end

tmp = SampleBuf(Array{eltype(src)}(framecount, nchannels(src)), samplerate(src))
n = unsafe_read!(src, tmp)
buf[(1:framecount)+frameoffset, :] = view(tmp.data, :, :)

n
end

# fallback function for libraries using the old unsafe_write function
const write_depwarned = Set{Type}()
function unsafe_write{T <: SampleSink}(sink::T, buf::Array, frameoffset, framecount)
if !(T in write_depwarned)
push!(write_depwarned, T)
warn("""`unsafe_write(src::$T, buf::Array, frameoffset, framecount)` not defined,
falling back to deprecated `unsafe_write(src::$T, buf::SampleBuf)`. Please
check the SampledSignals README for the new API""")
map(println, stacktrace())
end

tmp = SampleBuf(buf[(1:framecount)+frameoffset, :], samplerate(sink))
unsafe_write(sink, tmp)
end

const unit_depwarned = Ref(false)
function SampleBuf(arr::Array, sr::HertzQuantity)
if !unit_depwarned[]
warn("Samplerates with units are deprecated. Switch to plain floats")
map(println, stacktrace())
unit_depwarned[] = true
end
SampleBuf(arr, float(sr))
end

function SampleBuf(T::Type, sr::HertzQuantity, len::SecondsQuantity)
if !unit_depwarned[]
warn("Samplerates with units are deprecated. Switch to plain floats")
map(println, stacktrace())
unit_depwarned[] = true
end
SampleBuf(T, float(sr), len)
end

function SampleBuf(T::Type, sr::HertzQuantity, args...)
if !unit_depwarned[]
warn("Samplerates with units are deprecated. Switch to plain floats")
map(println, stacktrace())
unit_depwarned[] = true
end
SampleBuf(T, float(sr), args...)
end

# wrapper for samplerate that converts to floating point from unitful values
# and prints a depwarn. We use this internally to keep backwards compatibility,
# but it can be removed and replaced with normal `samplerate` calls when we
# remove compatibility
compat_samplerate(x) = warn_if_unitful(samplerate(x))

warn_if_unitful(x) = x
function warn_if_unitful(x::SIQuantity)
if !unit_depwarned[]
warn("Samplerates with units are deprecated. Switch to plain floats")
map(println, stacktrace())
unit_depwarned[] = true
end
float(x)
end

function SinSource{T <: SIQuantity}(eltype, samplerate::SIQuantity, freqs::Array{T})
SinSource(eltype, warn_if_unitful(samplerate), map(float, freqs))
end

function SinSource(eltype, samplerate::SIQuantity, freqs::Array)
SinSource(eltype, warn_if_unitful(samplerate), freqs)
end

function SinSource{T <: SIQuantity}(eltype, samplerate, freqs::Array{T})
SinSource(eltype, samplerate, map(warn_if_unitful, freqs))
end
22 changes: 0 additions & 22 deletions test/DummySampleStream.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,6 @@
DummyMonoSink() = DummySampleSink(DEFAULT_T, DEFAULT_SR, 1)
DummyStereoSink() = DummySampleSink(DEFAULT_T, DEFAULT_SR, 2)

@testset "write writes to buf" begin
sink = DummyStereoSink()
buf = SampleBuf(convert(Array{DEFAULT_T}, randn(32, 2)), DEFAULT_SR)
write(sink, buf)
@test sink.buf == buf.data
end

@testset "read reads from buf" begin
data = rand(DEFAULT_T, (64, 2))
source = DummySource(data)
buf = read(source, 64)
@test buf.data == data
end

@testset "read can read in seconds" begin
# fill with 1s of data
data = rand(DEFAULT_T, (DEFAULT_SR, 2))
source = DummySource(data)
buf = read(source, 0.0005s)
@test buf.data == data[1:round(Int, 0.0005*DEFAULT_SR), :]
end

@testset "supports audio interface" begin
data = rand(DEFAULT_T, (64, 2))
source = DummySource(data)
Expand Down
Loading

0 comments on commit c0ea736

Please sign in to comment.