diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml index f21b7bc..3ee2bd7 100644 --- a/.github/workflows/Tests.yml +++ b/.github/workflows/Tests.yml @@ -17,9 +17,9 @@ jobs: fail-fast: false matrix: version: + - '1.7' - '1.6' - '1' - - 'nightly' os: - ubuntu-latest - macOS-latest diff --git a/src/LibSndFile.jl b/src/LibSndFile.jl index c5ca292..4ef92ef 100644 --- a/src/LibSndFile.jl +++ b/src/LibSndFile.jl @@ -12,7 +12,10 @@ using libsndfile_jll: libsndfile const supported_formats = (format"WAV", format"FLAC", format"OGG") +include("constants.jl") +include("utils.jl") include("libsndfile_h.jl") +include("virtualio.jl") include("lengthIO.jl") include("sourcesink.jl") include("loadsave.jl") diff --git a/src/constants.jl b/src/constants.jl new file mode 100644 index 0000000..85c0e98 --- /dev/null +++ b/src/constants.jl @@ -0,0 +1,81 @@ +# Masks + +const SF_FORMAT_ENDMASK = 0x30000000 +const SF_FORMAT_TYPEMASK = 0x0FFF0000 +const SF_FORMAT_SUBMASK = 0x0000FFFF + +# Endian-ness options + +const SF_ENDIAN_FILE = 0x00000000 # Default file endian-ness. +const SF_ENDIAN_LITTLE = 0x10000000 # Force little endian-ness. +const SF_ENDIAN_BIG = 0x20000000 # Force big endian-ness. +const SF_ENDIAN_CPU = 0x30000000 # Force CPU endian-ness. + +# Major Formats + +const SF_FORMAT_WAV = 0x00010000 # Microsoft WAV format (little endian). +const SF_FORMAT_AIFF = 0x00020000 # Apple/SGI AIFF format (big endian). +const SF_FORMAT_AU = 0x00030000 # Sun/NeXT AU format (big endian). +const SF_FORMAT_RAW = 0x00040000 # RAW PCM data. +const SF_FORMAT_PAF = 0x00050000 # Ensoniq PARIS file format. +const SF_FORMAT_SVX = 0x00060000 # Amiga IFF / SVX8 / SV16 format. +const SF_FORMAT_NIST = 0x00070000 # Sphere NIST format. +const SF_FORMAT_VOC = 0x00080000 # VOC files. +const SF_FORMAT_IRCAM = 0x000A0000 # Berkeley/IRCAM/CARL +const SF_FORMAT_W64 = 0x000B0000 # Sonic Foundry's 64 bit RIFF/WAV +const SF_FORMAT_MAT4 = 0x000C0000 # Matlab (tm) V4.2 / GNU Octave 2.0 +const SF_FORMAT_MAT5 = 0x000D0000 # Matlab (tm) V5.0 / GNU Octave 2.1 +const SF_FORMAT_PVF = 0x000E0000 # Portable Voice Format +const SF_FORMAT_XI = 0x000F0000 # Fasttracker 2 Extended Instrument +const SF_FORMAT_HTK = 0x00100000 # HMM Tool Kit format +const SF_FORMAT_SDS = 0x00110000 # Midi Sample Dump Standard +const SF_FORMAT_AVR = 0x00120000 # Audio Visual Research +const SF_FORMAT_WAVEX = 0x00130000 # MS WAVE with WAVEFORMATEX +const SF_FORMAT_SD2 = 0x00160000 # Sound Designer 2 +const SF_FORMAT_FLAC = 0x00170000 # FLAC lossless file format +const SF_FORMAT_CAF = 0x00180000 # Core Audio File format +const SF_FORMAT_WVE = 0x00190000 # Psion WVE format +const SF_FORMAT_OGG = 0x00200000 # Xiph OGG container +const SF_FORMAT_MPC2K = 0x00210000 # Akai MPC 2000 sampler +const SF_FORMAT_RF64 = 0x00220000 # RF64 WAV file + +# SubFormats + +const SF_FORMAT_PCM_S8 = 0x00000001 # Signed 8 bit data +const SF_FORMAT_PCM_16 = 0x00000002 # Signed 16 bit data +const SF_FORMAT_PCM_24 = 0x00000003 # Signed 24 bit data +const SF_FORMAT_PCM_32 = 0x00000004 # Signed 32 bit data +const SF_FORMAT_PCM_U8 = 0x00000005 # Unsigned 8 bit data (WAV and RAW only) +const SF_FORMAT_FLOAT = 0x00000006 # 32 bit float data +const SF_FORMAT_DOUBLE = 0x00000007 # 64 bit float data +const SF_FORMAT_ULAW = 0x00000010 # U-Law encoded. +const SF_FORMAT_ALAW = 0x00000011 # A-Law encoded. +const SF_FORMAT_IMA_ADPCM = 0x00000012 # IMA ADPCM. +const SF_FORMAT_MS_ADPCM = 0x00000013 # Microsoft ADPCM. +const SF_FORMAT_GSM610 = 0x00000020 # GSM 6.10 encoding. +const SF_FORMAT_VOX_ADPCM = 0x00000021 # Oki Dialogic ADPCM encoding. +const SF_FORMAT_G721_32 = 0x00000030 # 32kbs G721 ADPCM encoding. +const SF_FORMAT_G723_24 = 0x00000031 # 24kbs G723 ADPCM encoding. +const SF_FORMAT_G723_40 = 0x00000032 # 40kbs G723 ADPCM encoding. +const SF_FORMAT_DWVW_12 = 0x00000040 # 12 bit Delta Width Variable Word encoding. +const SF_FORMAT_DWVW_16 = 0x00000041 # 16 bit Delta Width Variable Word encoding. +const SF_FORMAT_DWVW_24 = 0x00000042 # 24 bit Delta Width Variable Word encoding. +const SF_FORMAT_DWVW_N = 0x00000043 # N bit Delta Width Variable Word encoding. +const SF_FORMAT_DPCM_8 = 0x00000050 # 8 bit differential PCM (XI only) +const SF_FORMAT_DPCM_16 = 0x00000051 # 16 bit differential PCM (XI only) +const SF_FORMAT_VORBIS = 0x00000060 # Xiph Vorbis encoding. + +# Library flags + +# const SF_SEEK_SET = 0 +# const SF_SEEK_CUR = 1 +# const SF_SEEK_END = 2 + +const SFM_READ = Int32(0x10) +const SFM_WRITE = Int32(0x20) + +const SF_SEEK_SET = Int32(0) +const SF_SEEK_CUR = Int32(1) +const SF_SEEK_END = Int32(2) + +const sf_count_t = Int64 diff --git a/src/lengthIO.jl b/src/lengthIO.jl index 585d101..62842fa 100644 --- a/src/lengthIO.jl +++ b/src/lengthIO.jl @@ -4,10 +4,9 @@ mutable struct LengthIO{T<:IO} <: IO io::T length::Int64 + LengthIO(io::T,length::Integer) where {T} = new{T}(io, Int64(length)) end -LengthIO(io, l::Integer) = LengthIO(io, Int64(l)) - Base.length(io::LengthIO) = io.length for f in (:read, :read!, :write, :readbytes!, :unsafe_read, :unsafe_write, diff --git a/src/libsndfile_h.jl b/src/libsndfile_h.jl index 722c7a6..4dd1081 100644 --- a/src/libsndfile_h.jl +++ b/src/libsndfile_h.jl @@ -1,83 +1,3 @@ -# Masks - -const SF_FORMAT_ENDMASK = 0x30000000 -const SF_FORMAT_TYPEMASK = 0x0FFF0000 -const SF_FORMAT_SUBMASK = 0x0000FFFF - -# Endian-ness options - -const SF_ENDIAN_FILE = 0x00000000 # Default file endian-ness. -const SF_ENDIAN_LITTLE = 0x10000000 # Force little endian-ness. -const SF_ENDIAN_BIG = 0x20000000 # Force big endian-ness. -const SF_ENDIAN_CPU = 0x30000000 # Force CPU endian-ness. - -# Major Formats - -const SF_FORMAT_WAV = 0x00010000 # Microsoft WAV format (little endian). -const SF_FORMAT_AIFF = 0x00020000 # Apple/SGI AIFF format (big endian). -const SF_FORMAT_AU = 0x00030000 # Sun/NeXT AU format (big endian). -const SF_FORMAT_RAW = 0x00040000 # RAW PCM data. -const SF_FORMAT_PAF = 0x00050000 # Ensoniq PARIS file format. -const SF_FORMAT_SVX = 0x00060000 # Amiga IFF / SVX8 / SV16 format. -const SF_FORMAT_NIST = 0x00070000 # Sphere NIST format. -const SF_FORMAT_VOC = 0x00080000 # VOC files. -const SF_FORMAT_IRCAM = 0x000A0000 # Berkeley/IRCAM/CARL -const SF_FORMAT_W64 = 0x000B0000 # Sonic Foundry's 64 bit RIFF/WAV -const SF_FORMAT_MAT4 = 0x000C0000 # Matlab (tm) V4.2 / GNU Octave 2.0 -const SF_FORMAT_MAT5 = 0x000D0000 # Matlab (tm) V5.0 / GNU Octave 2.1 -const SF_FORMAT_PVF = 0x000E0000 # Portable Voice Format -const SF_FORMAT_XI = 0x000F0000 # Fasttracker 2 Extended Instrument -const SF_FORMAT_HTK = 0x00100000 # HMM Tool Kit format -const SF_FORMAT_SDS = 0x00110000 # Midi Sample Dump Standard -const SF_FORMAT_AVR = 0x00120000 # Audio Visual Research -const SF_FORMAT_WAVEX = 0x00130000 # MS WAVE with WAVEFORMATEX -const SF_FORMAT_SD2 = 0x00160000 # Sound Designer 2 -const SF_FORMAT_FLAC = 0x00170000 # FLAC lossless file format -const SF_FORMAT_CAF = 0x00180000 # Core Audio File format -const SF_FORMAT_WVE = 0x00190000 # Psion WVE format -const SF_FORMAT_OGG = 0x00200000 # Xiph OGG container -const SF_FORMAT_MPC2K = 0x00210000 # Akai MPC 2000 sampler -const SF_FORMAT_RF64 = 0x00220000 # RF64 WAV file - -# SubFormats - -const SF_FORMAT_PCM_S8 = 0x00000001 # Signed 8 bit data -const SF_FORMAT_PCM_16 = 0x00000002 # Signed 16 bit data -const SF_FORMAT_PCM_24 = 0x00000003 # Signed 24 bit data -const SF_FORMAT_PCM_32 = 0x00000004 # Signed 32 bit data -const SF_FORMAT_PCM_U8 = 0x00000005 # Unsigned 8 bit data (WAV and RAW only) -const SF_FORMAT_FLOAT = 0x00000006 # 32 bit float data -const SF_FORMAT_DOUBLE = 0x00000007 # 64 bit float data -const SF_FORMAT_ULAW = 0x00000010 # U-Law encoded. -const SF_FORMAT_ALAW = 0x00000011 # A-Law encoded. -const SF_FORMAT_IMA_ADPCM = 0x00000012 # IMA ADPCM. -const SF_FORMAT_MS_ADPCM = 0x00000013 # Microsoft ADPCM. -const SF_FORMAT_GSM610 = 0x00000020 # GSM 6.10 encoding. -const SF_FORMAT_VOX_ADPCM = 0x00000021 # Oki Dialogic ADPCM encoding. -const SF_FORMAT_G721_32 = 0x00000030 # 32kbs G721 ADPCM encoding. -const SF_FORMAT_G723_24 = 0x00000031 # 24kbs G723 ADPCM encoding. -const SF_FORMAT_G723_40 = 0x00000032 # 40kbs G723 ADPCM encoding. -const SF_FORMAT_DWVW_12 = 0x00000040 # 12 bit Delta Width Variable Word encoding. -const SF_FORMAT_DWVW_16 = 0x00000041 # 16 bit Delta Width Variable Word encoding. -const SF_FORMAT_DWVW_24 = 0x00000042 # 24 bit Delta Width Variable Word encoding. -const SF_FORMAT_DWVW_N = 0x00000043 # N bit Delta Width Variable Word encoding. -const SF_FORMAT_DPCM_8 = 0x00000050 # 8 bit differential PCM (XI only) -const SF_FORMAT_DPCM_16 = 0x00000051 # 16 bit differential PCM (XI only) -const SF_FORMAT_VORBIS = 0x00000060 # Xiph Vorbis encoding. - -# Library flags - -# const SF_SEEK_SET = 0 -# const SF_SEEK_CUR = 1 -# const SF_SEEK_END = 2 - -const SFM_READ = Int32(0x10) -const SFM_WRITE = Int32(0x20) - -const SF_SEEK_SET = Int32(0) -const SF_SEEK_CUR = Int32(1) -const SF_SEEK_END = Int32(2) - formatcode(::Type{format"WAV"}) = SF_FORMAT_WAV formatcode(::Type{format"FLAC"}) = SF_FORMAT_FLAC formatcode(::Type{format"OGG"}) = SF_FORMAT_OGG @@ -89,138 +9,59 @@ subformatcode(::Type{Float64}) = SF_FORMAT_DOUBLE """Take a LibSndFile format code and return a suitable sample type""" function fmt_to_type(fmt) - mapping = Dict{UInt32, Type}( - SF_FORMAT_PCM_S8 => PCM16Sample, - SF_FORMAT_PCM_U8 => PCM16Sample, - SF_FORMAT_PCM_16 => PCM16Sample, - SF_FORMAT_PCM_24 => PCM32Sample, - SF_FORMAT_PCM_32 => PCM32Sample, - SF_FORMAT_FLOAT => Float32, - SF_FORMAT_DOUBLE => Float64, - SF_FORMAT_VORBIS => Float32, - ) - - masked = fmt & SF_FORMAT_SUBMASK - masked in keys(mapping) || error("Format code $masked not recognized by LibSndFile.jl") - - mapping[masked] + mapping = Dict{UInt32, Type}( + SF_FORMAT_PCM_S8 => PCM16Sample, + SF_FORMAT_PCM_U8 => PCM16Sample, + SF_FORMAT_PCM_16 => PCM16Sample, + SF_FORMAT_PCM_24 => PCM32Sample, + SF_FORMAT_PCM_32 => PCM32Sample, + SF_FORMAT_FLOAT => Float32, + SF_FORMAT_DOUBLE => Float64, + SF_FORMAT_VORBIS => Float32, + ) + + masked = fmt & SF_FORMAT_SUBMASK + masked in keys(mapping) || error("Format code $masked not recognized by LibSndFile.jl") + + mapping[masked] end -const sf_count_t = Int64 - mutable struct SF_INFO - frames::sf_count_t - samplerate::Int32 - channels::Int32 - format::Int32 - sections::Int32 - seekable::Int32 + frames::sf_count_t + samplerate::Int32 + channels::Int32 + format::Int32 + sections::Int32 + seekable::Int32 end SF_INFO() = SF_INFO(0, 0, 0, 0, 0, 0) function sf_open(fname::String, mode, sfinfo) - filePtr = ccall((:sf_open, libsndfile), Ptr{Cvoid}, - (Cstring, Int32, Ref{SF_INFO}), - fname, mode, sfinfo) - - if filePtr == C_NULL - error("LibSndFile.jl error while opening $fname: ", sf_strerror(C_NULL)) - end - - filePtr -end - -# internals to get the virtual IO interface working -include("virtualio.jl") - -function sf_open(io::T, mode, sfinfo) where T <: IO - virtio = SF_VIRTUAL_IO(T) - filePtr = ccall((:sf_open_virtual, libsndfile), Ptr{Cvoid}, - (Ref{SF_VIRTUAL_IO}, Int32, Ref{SF_INFO}, Ptr{T}), - virtio, mode, sfinfo, pointer_from_objref(io)) - if filePtr == C_NULL - error("LibSndFile.jl error while opening stream: ", sf_strerror(C_NULL)) - end - - filePtr + ## this fixes #34 however is unstable and breaks test randomly + ## it's difficult to debug it without a Windows machine + #ptr = pointer(transcode(Cwchar_t,fname)) + #filePtr = ccall((:sf_wchar_open, libsndfile), Ptr{Cvoid}, + # (Cwstring, Int32, Ref{SF_INFO}), + # Cwstring(ptr), mode, sfinfo + filePtr = ccall((:sf_open, libsndfile), Ptr{Cvoid}, + (Cstring, Int32, Ref{SF_INFO}), + fname, mode, sfinfo) + if filePtr == C_NULL + error("LibSndFile.jl error while opening $fname: ", sf_strerror(C_NULL)) + end + + filePtr end function sf_close(filePtr) - err = ccall((:sf_close, libsndfile), Int32, (Ptr{Cvoid},), filePtr) - if err != 0 - error("LibSndFile.jl error: Failed to close file: ", sf_strerror(filePtr)) - end -end - -""" -Wrappers for the family of sf_readf_* functions, which read the given number -of frames into the given array. Returns the number of frames read. -""" -function sf_readf end - -sf_readf(filePtr, dest::Array{T}, nframes) where T <: Union{Int16, PCM16Sample} = - ccall((:sf_readf_short, libsndfile), Int64, - (Ptr{Cvoid}, Ptr{T}, Int64), - filePtr, dest, nframes) - -sf_readf(filePtr, dest::Array{T}, nframes) where T <: Union{Int32, PCM32Sample} = - ccall((:sf_readf_int, libsndfile), Int64, - (Ptr{Cvoid}, Ptr{T}, Int64), - filePtr, dest, nframes) - -sf_readf(filePtr, dest::Array{Float32}, nframes) = - ccall((:sf_readf_float, libsndfile), Int64, - (Ptr{Cvoid}, Ptr{Float32}, Int64), - filePtr, dest, nframes) - -sf_readf(filePtr, dest::Array{Float64}, nframes) = - ccall((:sf_readf_double, libsndfile), Int64, - (Ptr{Cvoid}, Ptr{Float64}, Int64), - filePtr, dest, nframes) - -""" -Wrappers for the family of sf_writef_* functions, which write the given number -of frames in the source array to the file. Returns the number of frames written. -""" -function sf_writef end - -sf_writef(filePtr, src::Array{T}, nframes) where T <: Union{Int16, PCM16Sample} = - ccall((:sf_writef_short, libsndfile), Int64, - (Ptr{Cvoid}, Ptr{T}, Int64), - filePtr, src, nframes) - -sf_writef(filePtr, src::Array{T}, nframes) where T <: Union{Int32, PCM32Sample} = - ccall((:sf_writef_int, libsndfile), Int64, - (Ptr{Cvoid}, Ptr{T}, Int64), - filePtr, src, nframes) - -sf_writef(filePtr, src::Array{Float32}, nframes) = - ccall((:sf_writef_float, libsndfile), Int64, - (Ptr{Cvoid}, Ptr{Float32}, Int64), - filePtr, src, nframes) - -sf_writef(filePtr, src::Array{Float64}, nframes) = - ccall((:sf_writef_double, libsndfile), Int64, - (Ptr{Cvoid}, Ptr{Float64}, Int64), - filePtr, src, nframes) - -function sf_strerror(filePtr) - errmsg = ccall((:sf_strerror, libsndfile), Ptr{UInt8}, (Ptr{Cvoid},), filePtr) - unsafe_string(errmsg) + err = ccall((:sf_close, libsndfile), Int32, (Ptr{Cvoid},), filePtr) + if err != 0 + error("LibSndFile.jl error: Failed to close file: ", sf_strerror(filePtr)) + end end sf_seek(filePtr, frames::sf_count_t, whence::Integer) = - ccall((:sf_seek, libsndfile), Int64, - (Ptr{Cvoid}, Int64, Int32), - filePtr, frames, whence) - -function version() - SFC_GET_LIB_VERSION = 0x1000 - buf = zeros(Cchar,256) - v = Cstring(pointer(buf)) - ccall((:sf_command, libsndfile), Int64, - (Ptr{Cvoid}, UInt, Cstring, UInt), - C_NULL, SFC_GET_LIB_VERSION, v, sizeof(buf)) - unsafe_string(v) -end +ccall((:sf_seek, libsndfile), Int64, + (Ptr{Cvoid}, Int64, Int32), + filePtr, frames, whence) diff --git a/src/readwrite.jl b/src/readwrite.jl index 95f5b1f..2ed092f 100644 --- a/src/readwrite.jl +++ b/src/readwrite.jl @@ -1,3 +1,29 @@ +""" +Wrappers for the family of sf_readf_* functions, which read the given number +of frames into the given array. Returns the number of frames read. +""" +function sf_readf end + +sf_readf(filePtr, dest::Array{T}, nframes) where T <: Union{Int16, PCM16Sample} = +ccall((:sf_readf_short, libsndfile), Int64, + (Ptr{Cvoid}, Ptr{T}, Int64), + filePtr, dest, nframes) + +sf_readf(filePtr, dest::Array{T}, nframes) where T <: Union{Int32, PCM32Sample} = +ccall((:sf_readf_int, libsndfile), Int64, + (Ptr{Cvoid}, Ptr{T}, Int64), + filePtr, dest, nframes) + +sf_readf(filePtr, dest::Array{Float32}, nframes) = +ccall((:sf_readf_float, libsndfile), Int64, + (Ptr{Cvoid}, Ptr{Float32}, Int64), + filePtr, dest, nframes) + +sf_readf(filePtr, dest::Array{Float64}, nframes) = +ccall((:sf_readf_double, libsndfile), Int64, + (Ptr{Cvoid}, Ptr{Float64}, Int64), + filePtr, dest, nframes) + function SampledSignals.unsafe_read!(source::SndFileSource, buf::Array, frameoffset, framecount) total = min(framecount, nframes(source) - source.pos + 1) nread = 0 @@ -20,6 +46,32 @@ function SampledSignals.unsafe_read!(source::SndFileSource, buf::Array, frameoff nread end +""" +Wrappers for the family of sf_writef_* functions, which write the given number +of frames in the source array to the file. Returns the number of frames written. +""" +function sf_writef end + +sf_writef(filePtr, src::Array{T}, nframes) where T <: Union{Int16, PCM16Sample} = +ccall((:sf_writef_short, libsndfile), Int64, + (Ptr{Cvoid}, Ptr{T}, Int64), + filePtr, src, nframes) + +sf_writef(filePtr, src::Array{T}, nframes) where T <: Union{Int32, PCM32Sample} = +ccall((:sf_writef_int, libsndfile), Int64, + (Ptr{Cvoid}, Ptr{T}, Int64), + filePtr, src, nframes) + +sf_writef(filePtr, src::Array{Float32}, nframes) = +ccall((:sf_writef_float, libsndfile), Int64, + (Ptr{Cvoid}, Ptr{Float32}, Int64), + filePtr, src, nframes) + +sf_writef(filePtr, src::Array{Float64}, nframes) = +ccall((:sf_writef_double, libsndfile), Int64, + (Ptr{Cvoid}, Ptr{Float64}, Int64), + filePtr, src, nframes) + # returns the number of samples written function SampledSignals.unsafe_write(sink::SndFileSink, buf::Array, frameoffset, framecount) nwritten = 0 diff --git a/src/utils.jl b/src/utils.jl new file mode 100644 index 0000000..d557831 --- /dev/null +++ b/src/utils.jl @@ -0,0 +1,14 @@ +function version() + SFC_GET_LIB_VERSION = 0x1000 + buf = zeros(Cchar,256) + v = Cstring(pointer(buf)) + ccall((:sf_command, libsndfile), Int64, + (Ptr{Cvoid}, UInt, Cstring, UInt), + C_NULL, SFC_GET_LIB_VERSION, v, sizeof(buf)) + unsafe_string(v) +end + +function sf_strerror(filePtr) + errmsg = ccall((:sf_strerror, libsndfile), Ptr{UInt8}, (Ptr{Cvoid},), filePtr) + unsafe_string(errmsg) +end diff --git a/src/virtualio.jl b/src/virtualio.jl index 2d62336..7d9dcb9 100644 --- a/src/virtualio.jl +++ b/src/virtualio.jl @@ -1,3 +1,13 @@ +# this contains a collection of function pointers that libsndfile uses to +# read and write data in a buffer +struct SF_VIRTUAL_IO + get_filelen::Ptr{Cvoid} + seek::Ptr{Cvoid} + read::Ptr{Cvoid} + write::Ptr{Cvoid} + tell::Ptr{Cvoid} +end + # libsndfile has the ability to define a virtual IO interface where you provide # callbacks for read, write, etc, and whenever the library wants to perform # these operations it calls your functions. See http://www.mega-nerd.com/libsndfile/api.html#open_virtual @@ -46,21 +56,12 @@ function virtual_write(src, count, userdata)::sf_count_t unsafe_write(io, src, count) count end + function virtual_tell(userdata)::sf_count_t io = unsafe_pointer_to_objref(userdata) position(io) end -# this contains a collection of function pointers that libsndfile uses to -# read and write data in a buffer -struct SF_VIRTUAL_IO - get_filelen::Ptr{Cvoid} - seek::Ptr{Cvoid} - read::Ptr{Cvoid} - write::Ptr{Cvoid} - tell::Ptr{Cvoid} -end - # make a struct of function pointers where the userdata argument is a pointer of # the specified type function SF_VIRTUAL_IO(::Type{T}) where T<:IO @@ -72,3 +73,15 @@ function SF_VIRTUAL_IO(::Type{T}) where T<:IO @cfunction(virtual_tell, sf_count_t, (Ptr{T}, )) ) end + +function sf_open(io::T, mode, sfinfo) where T <: IO + virtio = SF_VIRTUAL_IO(T) + filePtr = ccall((:sf_open_virtual, libsndfile), Ptr{Cvoid}, + (Ref{SF_VIRTUAL_IO}, Int32, Ref{SF_INFO}, Ptr{T}), + virtio, mode, sfinfo, pointer_from_objref(io)) + if filePtr == C_NULL + error("LibSndFile.jl error while opening stream: ", sf_strerror(C_NULL)) + end + + filePtr +end diff --git a/test/440left_880right_0.5amp.flac b/test/data/440left_880right_0.5amp.flac similarity index 100% rename from test/440left_880right_0.5amp.flac rename to test/data/440left_880right_0.5amp.flac diff --git a/test/440left_880right_0.5amp.ogg b/test/data/440left_880right_0.5amp.ogg similarity index 100% rename from test/440left_880right_0.5amp.ogg rename to test/data/440left_880right_0.5amp.ogg diff --git a/test/440left_880right_0.5amp.wav b/test/data/440left_880right_0.5amp.wav similarity index 100% rename from test/440left_880right_0.5amp.wav rename to test/data/440left_880right_0.5amp.wav diff --git a/test/440left_880right_0.5amp_double.wav b/test/data/440left_880right_0.5amp_double.wav similarity index 100% rename from test/440left_880right_0.5amp_double.wav rename to test/data/440left_880right_0.5amp_double.wav diff --git a/test/440left_880right_0.5amp_float.wav b/test/data/440left_880right_0.5amp_float.wav similarity index 100% rename from test/440left_880right_0.5amp_float.wav rename to test/data/440left_880right_0.5amp_float.wav diff --git a/test/440left_880right_0.5amp_pcm24.wav b/test/data/440left_880right_0.5amp_pcm24.wav similarity index 100% rename from test/440left_880right_0.5amp_pcm24.wav rename to test/data/440left_880right_0.5amp_pcm24.wav diff --git a/test/display.jl b/test/display.jl new file mode 100644 index 0000000..8a33c41 --- /dev/null +++ b/test/display.jl @@ -0,0 +1,62 @@ +@testset "Sink Display" begin + fname = string(tempname(), ".wav") + testbuf = SampleBuf(rand(Float32, 10000, 2) .- 0.5f0, srate) + # set up a 2-channel Float32 stream + stream = savestreaming_wav(fname, 2, srate, Float32) + io = IOBuffer() + show(io, stream) + @test replace(String(take!(io)), " " => "") == + replace( + """ + LibSndFile.SndFileSink{Float32, String} + path: "$fname" + channels: 2 + samplerate: 44100Hz + position: 0 of 0 frames + 0.00 of 0.00 seconds""" + , " " => "") + write(stream, testbuf) + show(io, stream) + @test replace(String(take!(io)), " " => "") == + replace( + """ + LibSndFile.SndFileSink{Float32, String} + path: "$fname" + channels: 2 + samplerate: 44100Hz + position: 10000 of 10000 frames + 0.23 of 0.23 seconds""" + , " " => "") +end + +@testset "Source Display" begin + fname = string(tempname(), ".wav") + testbuf = SampleBuf(rand(Float32, 10000, 2) .- 0.5f0, srate) + save_wav(fname, testbuf) + # set up a 2-channel Float32 stream + stream = loadstreaming_wav(fname) + io = IOBuffer() + show(io, stream) + @test replace(String(take!(io)), " " => "") == + replace( + """ + LibSndFile.SndFileSource{Float32, String} + path: "$fname" + channels: 2 + samplerate: 44100Hz + position: 0 of 10000 frames + 0.00 of 0.23 seconds""" + , " " => "") + read(stream, 5000) + show(io, stream) + @test replace(String(take!(io)), " " => "") == + replace( + """ + LibSndFile.SndFileSource{Float32, String} + path: "$fname" + channels: 2 + samplerate: 44100Hz + position: 5000 of 10000 frames + 0.11 of 0.23 seconds""" + , " " => "") +end diff --git a/test/file_reading.jl b/test/file_reading.jl new file mode 100644 index 0000000..c3d1903 --- /dev/null +++ b/test/file_reading.jl @@ -0,0 +1,63 @@ +@testset "PCM16 WAV file reading" begin + buf = load_wav(reference_wav) + @test samplerate(buf) == srate + @test nchannels(buf) == 2 + @test nframes(buf) == 100 + @test isapprox(domain(buf), collect(0:99)/(srate)) + @test mse(buf, reference_buf) < 1e-10 + @test eltype(buf) == PCM16Sample +end + +@testset "PCM32 WAV file reading" begin + buf = load_wav(reference_wav_pcm24) + @test samplerate(buf) == srate + @test nchannels(buf) == 2 + @test nframes(buf) == 100 + @test isapprox(domain(buf), collect(0:99)/(srate)) + @test mse(buf, reference_buf) < 1e-10 + @test eltype(buf) == PCM32Sample +end + +@testset "Float32 WAV file reading" begin + buf = load_wav(reference_wav_float) + @test samplerate(buf) == srate + @test nchannels(buf) == 2 + @test nframes(buf) == 100 + @test isapprox(domain(buf), collect(0:99)/(srate)) + @test mse(buf, reference_buf) < 1e-10 + @test eltype(buf) == Float32 +end + +@testset "Float64 WAV file reading" begin + buf = load_wav(reference_wav_double) + @test samplerate(buf) == srate + @test nchannels(buf) == 2 + @test nframes(buf) == 100 + @test isapprox(domain(buf), collect(0:99)/(srate)) + @test mse(buf, reference_buf) < 1e-10 + @test eltype(buf) == Float64 +end + +@testset "FLAC file reading" begin + buf = load_flac(reference_flac) + @test samplerate(buf) == srate + @test nchannels(buf) == 2 + @test nframes(buf) == 100 + @test isapprox(domain(buf), collect(0:99)/srate) + @test mse(buf, reference_buf) < 1e-10 + @test eltype(buf) == PCM16Sample +end + +@testset "OGG file reading" begin + buf = load_ogg(reference_ogg) + @test samplerate(buf) == srate + @test nchannels(buf) == 2 + @test nframes(buf) == 100 + @test isapprox(domain(buf), collect(0:99)/srate) + # lossy compression, so relax the accuracy a bit + @test mse(buf, reference_buf) < 1e-5 +end + +@testset "Read errors" begin + @test_throws ErrorException load_wav("doesnotexist.wav") +end diff --git a/test/file_writing.jl b/test/file_writing.jl new file mode 100644 index 0000000..6a598e0 --- /dev/null +++ b/test/file_writing.jl @@ -0,0 +1,92 @@ +@testset "WAV file writing (float64)" begin + fname = string(tempname(), ".wav") + testbuf = SampleBuf(rand(100, 2) .- 0.5, srate) + save_wav(fname, testbuf) + buf = load_wav(fname) + @test eltype(buf) == eltype(testbuf) + @test samplerate(buf) == srate + @test nchannels(buf) == 2 + @test nframes(buf) == 100 + @test isapprox(domain(buf), collect(0:99)/srate) + @test mse(buf, testbuf) < 1e-10 +end + +@testset "WAV file writing (float32)" begin + fname = string(tempname(), ".wav") + testbuf = SampleBuf(rand(Float32, 100, 2) .- 0.5f0, srate) + save_wav(fname, testbuf) + buf = load_wav(fname) + @test eltype(buf) == eltype(testbuf) + @test samplerate(buf) == srate + @test nchannels(buf) == 2 + @test nframes(buf) == 100 + @test isapprox(domain(buf), collect(0:99)/srate) + @test mse(buf, testbuf) < 1e-10 +end + +@testset "OGG file writing" begin + fname = string(tempname(), ".ogg") + testbuf = SampleBuf(rand(Float32, 100, 2) .- 0.5, srate) + save_ogg(fname, testbuf) + buf = load_ogg(fname) + @test samplerate(buf) == srate + @test nchannels(buf) == 2 + @test nframes(buf) == 100 + @test isapprox(domain(buf), collect(0:99)/srate) + # noise doesn't compress very well... + @test mse(buf, testbuf) < 0.05 +end + +@testset "FLAC file writing" begin + fname = string(tempname(), ".flac") + arr = map(PCM16Sample, rand(100, 2) .- 0.5) + testbuf = SampleBuf(arr, srate) + save_flac(fname, testbuf) + buf = load_flac(fname) + @test samplerate(buf) == srate + @test nchannels(buf) == 2 + @test nframes(buf) == 100 + @test isapprox(domain(buf), collect(0:99)/srate) + @test mse(buf, testbuf) < 1e-10 +end + +@testset "Writing $T data" for T in [PCM16Sample, PCM32Sample, Float32, Float64] + fname = string(tempname(), ".wav") + arr = map(T, rand(100, 2) .- 0.5) + testbuf = SampleBuf(arr, srate) + save_wav(fname, testbuf) + buf = load_wav(fname) + @test eltype(buf) == T + @test mse(buf, testbuf) < 1e-10 +end + +@testset "Streaming writing" begin + fname = string(tempname(), ".wav") + testbuf = SampleBuf(rand(Float32, 100, 2) .- 0.5, srate) + # set up a 2-channel Float32 stream + stream = savestreaming_wav(fname, 2, srate, Float32) + write(stream, testbuf[1:50, :]) + write(stream, testbuf[51:100, :]) + close(stream) + buf = load_wav(fname) + @test mse(buf, testbuf) < 1e-10 + + # now with do syntax + fname = string(tempname(), ".wav") + testbuf = SampleBuf(rand(Float32, 100, 2) .- 0.5, srate) + # set up a 2-channel Float32 stream + savestreaming_wav(fname, 2, srate, Float32) do stream + write(stream, testbuf[1:50, :]) + write(stream, testbuf[51:100, :]) + end + buf = load_wav(fname) + @test mse(buf, testbuf) < 1e-10 + +end + +@testset "Write errors" begin + testbuf = SampleBuf(rand(Float32, 100, 2) .- 0.5, srate) + flacname = string(tempname(), ".flac") + @test_throws ErrorException save_flac(abspath("doesnotexist.wav"), testbuf) + @test_throws ErrorException save_flac(flacname, testbuf) +end diff --git a/test/fileio.jl b/test/fileio.jl new file mode 100644 index 0000000..0b1663c --- /dev/null +++ b/test/fileio.jl @@ -0,0 +1,15 @@ +arr = map(PCM16Sample, rand(100, 2) .- 0.5) +testbuf = SampleBuf(arr, srate) +for ext in extensions + fname = string(tempname(), ext) + FileIO.save(fname, testbuf) + buf = FileIO.load(fname) + @test buf isa SampleBuf +end + +if !Sys.iswindows() + # testing with unicode + file = joinpath(tempdir(),"α.flac") + FileIO.save(file, testbuf) + FileIO.load(file) +end diff --git a/test/runtests.jl b/test/runtests.jl index 1e38d18..7ec1ea9 100755 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,415 +1,41 @@ -#!/usr/bin/env julia - using Test -using FileIO: File, Stream, @format_str -import FileIO -import LibSndFile +using FileIO +using LibSndFile using SampledSignals +using LinearAlgebra -include("testhelpers.jl") - -# define some loaders and savers that bypass FileIO's detection machinery, of -# the form: -# load_wav(io::String, args...) = LibSndFile.load(File(format"WAV", io), args...) -# load_wav(io::IO, args...) = LibSndFile.load(Stream(format"WAV", io), args...) -# also create do-compatible methods of the form: -# function loadstreaming_wav(dofunc::Function, io::IO, args...) -# str = LibSndFile.load(dofunc, Stream(format"WAV", io), args...) -# try -# dofunc(str) -# finally -# close(str) -# end -# end +extensions = (".wav", + ".ogg", + ".flac") -for f in (:load, :save, :loadstreaming, :savestreaming) - for io in ((String, File), (IO, Stream)) - for fmt in (("_wav", format"WAV"), ("_ogg", format"OGG"), ("_flac", format"FLAC")) - @eval $(Symbol(f, fmt[1]))(io::$(io[1]), args...) = - LibSndFile.$f($(io[2]){$(fmt[2])}( io), args...) - if f in (:loadstreaming, :savestreaming) - @eval function $(Symbol(f, fmt[1]))(dofunc::Function, io::$(io[1]), args...) - str = LibSndFile.$f($(io[2]){$(fmt[2])}( io), args...) - try - dofunc(str) - finally - close(str) - end - end - end - end - end -end +formats = [ + ("_wav", format"WAV"), + ("_ogg", format"OGG"), + ("_flac", format"FLAC"), + ] -"""Generates a 100-sample 2-channel signal""" -function gen_reference(srate) - t = collect(0:99) / srate - phase = [2pi*440t 2pi*880t] +include("utils.jl") - 0.5sin.(phase) -end - -srate = 44100 -# reference file generated with Audacity. Careful to turn dithering off -# on export for deterministic output! -reference_wav = joinpath(dirname(@__FILE__), "440left_880right_0.5amp.wav") -reference_wav_float = joinpath(dirname(@__FILE__), "440left_880right_0.5amp_float.wav") -reference_wav_double = joinpath(dirname(@__FILE__), "440left_880right_0.5amp_double.wav") -reference_wav_pcm24 = joinpath(dirname(@__FILE__), "440left_880right_0.5amp_pcm24.wav") -reference_ogg = joinpath(dirname(@__FILE__), "440left_880right_0.5amp.ogg") -reference_flac = joinpath(dirname(@__FILE__), "440left_880right_0.5amp.flac") -reference_buf = gen_reference(srate) - -# don't indent the individual testsets so we can more easily run them from -# Juno @testset "LibSndFile Tests" begin - -@testset "Version" begin - v = LibSndFile.version() - @test v[1:10] == "libsndfile" -end - -@testset "Read errors" begin - @test_throws ErrorException load_wav("doesnotexist.wav") -end - -@testset "WAV file reading" begin - buf = load_wav(reference_wav) - @test samplerate(buf) == srate - @test nchannels(buf) == 2 - @test nframes(buf) == 100 - @test isapprox(domain(buf), collect(0:99)/(srate)) - @test mse(buf, reference_buf) < 1e-10 -end - -@testset "Reading different sample types" begin - buf = load_wav(reference_wav) - @test eltype(buf) == PCM16Sample - - buf_float = load_wav(reference_wav_float) - @test eltype(buf_float) == Float32 - @test mse(buf_float, reference_buf) < 1e-10 - - buf_double = load_wav(reference_wav_double) - @test eltype(buf_double) == Float64 - @test mse(buf_double, reference_buf) < 1e-10 - - buf_pcm24 = load_wav(reference_wav_pcm24) - @test eltype(buf_pcm24) == PCM32Sample - @test mse(buf_pcm24, reference_buf) < 1e-10 -end - -@testset "FLAC file reading" begin - buf = load_flac(reference_flac) - @test samplerate(buf) == srate - @test nchannels(buf) == 2 - @test nframes(buf) == 100 - @test isapprox(domain(buf), collect(0:99)/srate) - @test mse(buf, reference_buf) < 1e-10 -end - -@testset "OGG file reading" begin - buf = load_ogg(reference_ogg) - @test samplerate(buf) == srate - @test nchannels(buf) == 2 - @test nframes(buf) == 100 - @test isapprox(domain(buf), collect(0:99)/srate) - # lossy compression, so relax the accuracy a bit - @test mse(buf, reference_buf) < 1e-5 -end - -@testset "Streaming reading" begin - str = loadstreaming_wav(reference_wav) - @test nframes(str) == 100 - @test position(str) == 1 - @test mse(read(str, 50), reference_buf[1:50, :]) < 1e-10 - @test mse(read(str, 50), reference_buf[51:100, :]) < 1e-10 - close(str) - # now with do syntax - loadstreaming_wav(reference_wav) do str - @test mse(read(str, 50), reference_buf[1:50, :]) < 1e-10 - @test mse(read(str, 50), reference_buf[51:100, :]) < 1e-10 - end - # now try reading all at once - loadstreaming_wav(reference_wav) do str - @test mse(read(str), reference_buf) < 1e-10 - end - - # seeking - loadstreaming_wav(reference_wav) do str - seek(str, 22) - @test mse(read(str), reference_buf[22:end, :]) < 1e-10 - end - - # skipping - loadstreaming_wav(reference_wav) do str - seek(str, 22) - skip(str, 10) - @test mse(read(str), reference_buf[32:end, :]) < 1e-10 - end - + @testset "Version" begin + v = LibSndFile.version() + @test v[1:10] == "libsndfile" + end + @testset "File reading" begin + include("file_reading.jl") + end + @testset "Streaming and IO" begin + include("stream.jl") + end + @testset "File writing" begin + include("file_writing.jl") + end + @testset "FileIO Integration" begin + include("fileio.jl") + end + @testset "Display" begin + include("display.jl") + end + # TODO: check out what happens when samplerate, channels, etc. are wrong + # when reading/writing end - -@testset "Reading from IO Stream" begin - open(reference_wav) do io - loadstreaming_wav(io) do str - @test nframes(str) == 100 - @test mse(read(str), reference_buf) < 1e-10 - end - end - open(reference_wav) do io - buf = load_wav(io) - @test samplerate(buf) == srate - @test nchannels(buf) == 2 - @test nframes(buf) == 100 - @test isapprox(domain(buf), collect(0:99)/(srate)) - @test mse(buf, reference_buf) < 1e-10 - end -end - -@testset "Writing to IO Streams" begin - fname = string(tempname(), ".wav") - testbuf = SampleBuf(rand(Float32, 100, 2) .- 0.5, srate) - open(fname, "w") do io - savestreaming_wav(io, 2, srate, Float32) do str - write(str, testbuf[1:50, :]) - write(str, testbuf[51:100, :]) - end - end - @test load_wav(fname) == testbuf - fname = string(tempname(), ".wav") - open(fname, "w") do io - save_wav(io, testbuf) - end - @test load_wav(fname) == testbuf -end - -@testset "Supports IOBuffer" begin - io = IOBuffer() - testbuf = SampleBuf(rand(100, 2) .- 0.5, srate) - save_wav(io, testbuf) - seek(io, 0) - @test load_wav(io) == testbuf -end - -@testset "WAV file writing (float64)" begin - fname = string(tempname(), ".wav") - testbuf = SampleBuf(rand(100, 2) .- 0.5, srate) - save_wav(fname, testbuf) - buf = load_wav(fname) - @test eltype(buf) == eltype(testbuf) - @test samplerate(buf) == srate - @test nchannels(buf) == 2 - @test nframes(buf) == 100 - @test isapprox(domain(buf), collect(0:99)/srate) - @test mse(buf, testbuf) < 1e-10 -end - -@testset "WAV file writing (float32)" begin - fname = string(tempname(), ".wav") - testbuf = SampleBuf(rand(Float32, 100, 2) .- 0.5f0, srate) - save_wav(fname, testbuf) - buf = load_wav(fname) - @test eltype(buf) == eltype(testbuf) - @test samplerate(buf) == srate - @test nchannels(buf) == 2 - @test nframes(buf) == 100 - @test isapprox(domain(buf), collect(0:99)/srate) - @test mse(buf, testbuf) < 1e-10 -end - -@testset "OGG file writing" begin - fname = string(tempname(), ".ogg") - testbuf = SampleBuf(rand(Float32, 100, 2) .- 0.5, srate) - save_ogg(fname, testbuf) - buf = load_ogg(fname) - @test samplerate(buf) == srate - @test nchannels(buf) == 2 - @test nframes(buf) == 100 - @test isapprox(domain(buf), collect(0:99)/srate) - # noise doesn't compress very well... - @test mse(buf, testbuf) < 0.05 -end - -@testset "FLAC file writing" begin - fname = string(tempname(), ".flac") - arr = map(PCM16Sample, rand(100, 2) .- 0.5) - testbuf = SampleBuf(arr, srate) - save_flac(fname, testbuf) - buf = load_flac(fname) - @test samplerate(buf) == srate - @test nchannels(buf) == 2 - @test nframes(buf) == 100 - @test isapprox(domain(buf), collect(0:99)/srate) - @test mse(buf, testbuf) < 1e-10 -end - -@testset "Writing $T data" for T in [PCM16Sample, PCM32Sample, Float32, Float64] - fname = string(tempname(), ".wav") - arr = map(T, rand(100, 2) .- 0.5) - testbuf = SampleBuf(arr, srate) - save_wav(fname, testbuf) - buf = load_wav(fname) - @test eltype(buf) == T - @test mse(buf, testbuf) < 1e-10 -end - -@testset "Write errors" begin - testbuf = SampleBuf(rand(Float32, 100, 2) .- 0.5, srate) - flacname = string(tempname(), ".flac") - @test_throws ErrorException save_flac(abspath("doesnotexist.wav"), testbuf) - @test_throws ErrorException save_flac(flacname, testbuf) -end - -@testset "Streaming writing" begin - fname = string(tempname(), ".wav") - testbuf = SampleBuf(rand(Float32, 100, 2) .- 0.5, srate) - # set up a 2-channel Float32 stream - stream = savestreaming_wav(fname, 2, srate, Float32) - write(stream, testbuf[1:50, :]) - write(stream, testbuf[51:100, :]) - close(stream) - buf = load_wav(fname) - @test mse(buf, testbuf) < 1e-10 - - # now with do syntax - fname = string(tempname(), ".wav") - testbuf = SampleBuf(rand(Float32, 100, 2) .- 0.5, srate) - # set up a 2-channel Float32 stream - savestreaming_wav(fname, 2, srate, Float32) do stream - write(stream, testbuf[1:50, :]) - write(stream, testbuf[51:100, :]) - end - buf = load_wav(fname) - @test mse(buf, testbuf) < 1e-10 - -end - -@testset "FileIO Integration" begin - arr = map(PCM16Sample, rand(100, 2) .- 0.5) - testbuf = SampleBuf(arr, srate) - for ext in (".wav", ".ogg", ".flac") - fname = string(tempname(), ext) - FileIO.save(fname, testbuf) - buf = FileIO.load(fname) - @test buf isa SampleBuf - end -end - -@testset "Sink Display" begin - fname = string(tempname(), ".wav") - testbuf = SampleBuf(rand(Float32, 10000, 2) .- 0.5f0, srate) - # set up a 2-channel Float32 stream - stream = savestreaming_wav(fname, 2, srate, Float32) - io = IOBuffer() - show(io, stream) - @test replace(String(take!(io)), " " => "") == - replace( - """ - LibSndFile.SndFileSink{Float32, String} - path: "$fname" - channels: 2 - samplerate: 44100Hz - position: 0 of 0 frames - 0.00 of 0.00 seconds""" - , " " => "") - write(stream, testbuf) - show(io, stream) - @test replace(String(take!(io)), " " => "") == - replace( - """ - LibSndFile.SndFileSink{Float32, String} - path: "$fname" - channels: 2 - samplerate: 44100Hz - position: 10000 of 10000 frames - 0.23 of 0.23 seconds""" - , " " => "") -end - -@testset "Source Display" begin - fname = string(tempname(), ".wav") - testbuf = SampleBuf(rand(Float32, 10000, 2) .- 0.5f0, srate) - save_wav(fname, testbuf) - # set up a 2-channel Float32 stream - stream = loadstreaming_wav(fname) - io = IOBuffer() - show(io, stream) - @test replace(String(take!(io)), " " => "") == - replace( - """ - LibSndFile.SndFileSource{Float32, String} - path: "$fname" - channels: 2 - samplerate: 44100Hz - position: 0 of 10000 frames - 0.00 of 0.23 seconds""" - , " " => "") - read(stream, 5000) - show(io, stream) - @test replace(String(take!(io)), " " => "") == - replace( - """ - LibSndFile.SndFileSource{Float32, String} - path: "$fname" - channels: 2 - samplerate: 44100Hz - position: 5000 of 10000 frames - 0.11 of 0.23 seconds""" - , " " => "") -end - -# TODO: check out what happens when samplerate, channels, etc. are wrong -# when reading/writing - -# -# # test seeking -# -# # test rendering as an AudioNode -# AudioIO.open(fname) do f -# # pretend we have a stream at the same rate as the file -# bufsize = 1024 -# input = zeros(AudioSample, bufsize) -# test_info = DeviceInfo(srate, bufsize) -# node = FilePlayer(f) -# # convert to floating point because that's what AudioIO uses natively -# expected = convert(AudioBuf, reference ./ (2^15)) -# buf = render(node, input, test_info) -# @fact expected[1:bufsize] => buf[1:bufsize] -# buf = render(node, input, test_info) -# @fact expected[bufsize+1:2*bufsize] => buf[1:bufsize] -# end -# end -# -# @testset "Stereo file reading" begin -# fname = Pkg.dir("AudioIO", "test", "440left_880right.wav") -# srate = 44100 -# t = [0 : 2 * srate - 1] / srate -# expected = int16((2^15-1) * hcat(sin(2pi*t*440), sin(2pi*t*880))) -# -# AudioIO.open(fname) do f -# buf = read(f) -# @fact buf => mse(expected, 5) -# end -# end -# -# # note - currently AudioIO just mixes down to Mono. soon we'll support this -# # new-fangled stereo sound stuff -# @testset "Stereo file rendering" begin -# fname = Pkg.dir("AudioIO", "test", "440left_880right.wav") -# srate = 44100 -# bufsize = 1024 -# input = zeros(AudioSample, bufsize) -# test_info = DeviceInfo(srate, bufsize) -# t = [0 : 2 * srate - 1] / srate -# expected = convert(AudioBuf, 0.5 * (sin(2pi*t*440) + sin(2pi*t*880))) -# -# AudioIO.open(fname) do f -# node = FilePlayer(f) -# buf = render(node, input, test_info) -# @fact buf[1:bufsize] => mse(expected[1:bufsize]) -# buf = render(node, input, test_info) -# @fact buf[1:bufsize] => mse(expected[bufsize+1:2*bufsize]) -# end -# end -end # @testset LibSndFile diff --git a/test/stream.jl b/test/stream.jl new file mode 100644 index 0000000..7283623 --- /dev/null +++ b/test/stream.jl @@ -0,0 +1,73 @@ +@testset "Streaming reading" begin + str = loadstreaming_wav(reference_wav) + @test nframes(str) == 100 + @test position(str) == 1 + @test mse(read(str, 50), reference_buf[1:50, :]) < 1e-10 + @test mse(read(str, 50), reference_buf[51:100, :]) < 1e-10 + close(str) + # now with do syntax + loadstreaming_wav(reference_wav) do str + @test mse(read(str, 50), reference_buf[1:50, :]) < 1e-10 + @test mse(read(str, 50), reference_buf[51:100, :]) < 1e-10 + end + # now try reading all at once + loadstreaming_wav(reference_wav) do str + @test mse(read(str), reference_buf) < 1e-10 + end + + # seeking + loadstreaming_wav(reference_wav) do str + seek(str, 22) + @test mse(read(str), reference_buf[22:end, :]) < 1e-10 + end + + # skipping + loadstreaming_wav(reference_wav) do str + seek(str, 22) + skip(str, 10) + @test mse(read(str), reference_buf[32:end, :]) < 1e-10 + end + +end + +@testset "Reading from IO Stream" begin + open(reference_wav) do io + loadstreaming_wav(io) do str + @test nframes(str) == 100 + @test mse(read(str), reference_buf) < 1e-10 + end + end + open(reference_wav) do io + buf = load_wav(io) + @test samplerate(buf) == srate + @test nchannels(buf) == 2 + @test nframes(buf) == 100 + @test isapprox(domain(buf), collect(0:99)/(srate)) + @test mse(buf, reference_buf) < 1e-10 + end +end + +@testset "Writing to IO Streams" begin + fname = string(tempname(), ".wav") + testbuf = SampleBuf(rand(Float32, 100, 2) .- 0.5, srate) + open(fname, "w") do io + savestreaming_wav(io, 2, srate, Float32) do str + write(str, testbuf[1:50, :]) + write(str, testbuf[51:100, :]) + end + end + @test load_wav(fname) == testbuf + fname = string(tempname(), ".wav") + open(fname, "w") do io + save_wav(io, testbuf) + end + @test load_wav(fname) == testbuf +end + +@testset "Supports IOBuffer" begin + io = IOBuffer() + testbuf = SampleBuf(rand(100, 2) .- 0.5, srate) + save_wav(io, testbuf) + seek(io, 0) + @test load_wav(io) == testbuf +end diff --git a/test/testhelpers.jl b/test/testhelpers.jl deleted file mode 100644 index 98a4164..0000000 --- a/test/testhelpers.jl +++ /dev/null @@ -1,12 +0,0 @@ -# convenience function to calculate the mean-squared error -function mse(arr1::AbstractArray, arr2::AbstractArray) - if size(arr1) != size(arr2) - throw(DimensionMismatch("Got $(size(arr1)) and $(size(arr2))")) - end - N = length(arr1) - err = 0.0 - for i in 1:N - err += (arr2[i] - arr1[i])^2 - end - err /= N -end diff --git a/test/utils.jl b/test/utils.jl new file mode 100644 index 0000000..7687f1f --- /dev/null +++ b/test/utils.jl @@ -0,0 +1,53 @@ +srate = 44100 +# convenience function to calculate the mean-squared error +mse(x,y) = norm(x-y)^2/length(x) + +# Generates a 100-sample 2-channel signal +function gen_reference(srate) + t = collect(0:99) / srate + phase = [2pi*440t 2pi*880t] + 0.5sin.(phase) +end + +# reference file generated with Audacity. Careful to turn dithering off +# on export for deterministic output! +reference_wav = joinpath("data", "440left_880right_0.5amp.wav") +reference_wav_float = joinpath("data", "440left_880right_0.5amp_float.wav") +reference_wav_double = joinpath("data","440left_880right_0.5amp_double.wav") +reference_wav_pcm24 = joinpath("data", "440left_880right_0.5amp_pcm24.wav") +reference_ogg = joinpath("data", "440left_880right_0.5amp.ogg") +reference_flac = joinpath("data", "440left_880right_0.5amp.flac") +reference_buf = gen_reference(srate) + +# define some loaders and savers that bypass FileIO's detection machinery, of +# the form: +# load_wav(io::String, args...) = LibSndFile.load(File(format"WAV", io), args...) +# load_wav(io::IO, args...) = LibSndFile.load(Stream(format"WAV", io), args...) +# also create do-compatible methods of the form: +# function loadstreaming_wav(dofunc::Function, io::IO, args...) +# str = LibSndFile.load(dofunc, Stream(format"WAV", io), args...) +# try +# dofunc(str) +# finally +# close(str) +# end +# end + +for f in (:load, :save, :loadstreaming, :savestreaming) + for io in ((String, File), (IO, Stream)) + for fmt in formats + @eval $(Symbol(f, fmt[1]))(io::$(io[1]), args...) = + LibSndFile.$f($(io[2]){$(fmt[2])}( io), args...) + if f in (:loadstreaming, :savestreaming) + @eval function $(Symbol(f, fmt[1]))(dofunc::Function, io::$(io[1]), args...) + str = LibSndFile.$f($(io[2]){$(fmt[2])}( io), args...) + try + dofunc(str) + finally + close(str) + end + end + end + end + end +end