#!/usr/bin/env julia using Base.Sys: iswindows using Documenter: doctest using PortAudio: combine_default_sample_rates, devices, get_default_input_index, get_default_output_index, get_device, get_input_type, get_output_type, handle_status, initialize, PortAudioException, PortAudio, PortAudioDevice, PortAudioStream, safe_load, seek_alsa_conf, terminate, name using PortAudio.LibPortAudio: Pa_AbortStream, PaError, PaErrorCode, paFloat32, Pa_GetDefaultHostApi, Pa_GetDeviceInfo, Pa_GetHostApiCount, Pa_GetLastHostErrorInfo, Pa_GetSampleSize, Pa_GetStreamCpuLoad, Pa_GetStreamInfo, Pa_GetStreamReadAvailable, Pa_GetStreamTime, Pa_GetStreamWriteAvailable, Pa_GetVersionInfo, Pa_HostApiDeviceIndexToDeviceIndex, paHostApiNotFound, Pa_HostApiTypeIdToHostApiIndex, PaHostErrorInfo, paInDevelopment, paInvalidDevice, Pa_IsFormatSupported, Pa_IsStreamActive, paNoError, paNoFlag, paNotInitialized, Pa_OpenDefaultStream, paOutputUnderflowed, Pa_SetStreamFinishedCallback, Pa_Sleep, Pa_StopStream, PaStream, PaStreamInfo, PaStreamParameters, PaVersionInfo using SampledSignals: nchannels, s, SampleBuf, samplerate, SinSource using Test: @test, @test_logs, @test_nowarn, @testset, @test_throws @testset "Tests without sound" begin @testset "Reports version" begin io = IOBuffer() PortAudio.versioninfo(io) result = split(String(take!((io))), "\n") # make sure this is the same version I tested with @test startswith(result[1], "PortAudio V19") end @testset "Can list devices without crashing" begin display(devices()) println() end @testset "libortaudio without sound" begin @test handle_status(Pa_GetHostApiCount()) >= 0 @test handle_status(Pa_GetDefaultHostApi()) >= 0 # version info not available on windows? if !Sys.iswindows() @test safe_load(Pa_GetVersionInfo(), ErrorException("no info")) isa PaVersionInfo end @test safe_load(Pa_GetLastHostErrorInfo(), ErrorException("no info")) isa PaHostErrorInfo @test PaErrorCode(Pa_IsFormatSupported(C_NULL, C_NULL, 0.0)) == paInvalidDevice @test PaErrorCode( Pa_OpenDefaultStream(Ref(C_NULL), 0, 0, paFloat32, 0.0, 0, C_NULL, C_NULL), ) == paInvalidDevice end @testset "Errors without sound" begin @test sprint(showerror, PortAudioException(paNotInitialized)) == "PortAudioException: PortAudio not initialized" @test_throws KeyError("foobarbaz") get_device("foobarbaz") @test_throws KeyError(-1) get_device(-1) @test_throws ArgumentError("Could not find alsa.conf in ()") seek_alsa_conf(()) @test_logs (:warn, "libportaudio: Output underflowed") handle_status( PaError(paOutputUnderflowed), ) @test_throws PortAudioException(paNotInitialized) handle_status( PaError(paNotInitialized), ) Pa_Sleep(1) @test Pa_GetSampleSize(paFloat32) == 4 end # make sure we can terminate, then reinitialize terminate() initialize() end if !isempty(devices()) @testset "Tests with sound" begin # these default values are specific to local machines input_name = get_device(get_default_input_index()).name output_name = get_device(get_default_output_index()).name @testset "Interactive tests" begin println("Recording...") stream = PortAudioStream(input_name, output_name, 2, 0; adjust_channels = true) buffer = read(stream, 5s) @test size(buffer) == (round(Int, 5 * samplerate(stream)), nchannels(stream.source)) close(stream) sleep(1) println("Playing back recording...") PortAudioStream(input_name, output_name, 0, 2; adjust_channels = true) do stream write(stream, buffer) end sleep(1) println("Testing pass-through") stream = PortAudioStream(input_name, output_name, 2, 2; adjust_channels = true) sink = stream.sink source = stream.source @test sprint(show, stream) == """ PortAudioStream{Float32} Samplerate: 44100Hz 2 channel sink: $(repr(output_name)) 2 channel source: $(repr(input_name))""" @test sprint(show, source) == "2 channel source: $(repr(input_name))" @test sprint(show, sink) == "2 channel sink: $(repr(output_name))" write(stream, stream, 5s) @test PaErrorCode(handle_status(Pa_StopStream(stream.pointer_to))) == paNoError @test isopen(stream) close(stream) sleep(1) @test !isopen(stream) @test !isopen(sink) @test !isopen(source) println("done") end @testset "Samplerate-converting writing" begin PortAudioStream(input_name, output_name, 0, 2; adjust_channels = true) do stream write( stream, SinSource(eltype(stream), samplerate(stream) * 0.8, [220, 330]), 3s, ) println("expected blip") write( stream, SinSource(eltype(stream), samplerate(stream) * 1.2, [220, 330]), 3s, ) end end sleep(1) # no way to check that the right data is actually getting read or written here, # but at least it's not crashing. @testset "Queued Writing" begin PortAudioStream(input_name, output_name, 0, 2; adjust_channels = true) do stream buffer = SampleBuf( rand(eltype(stream), 48000, nchannels(stream.sink)) * 0.1, samplerate(stream), ) frame_count_1 = @async write(stream, buffer) frame_count_2 = @async write(stream, buffer) @test fetch(frame_count_1) == 48000 println("expected blip") @test fetch(frame_count_2) == 48000 end sleep(1) end @testset "Queued Reading" begin PortAudioStream(input_name, output_name, 2, 0; adjust_channels = true) do stream buffer = SampleBuf( rand(eltype(stream), 48000, nchannels(stream.source)) * 0.1, samplerate(stream), ) frame_count_1 = @async read!(stream, buffer) frame_count_2 = @async read!(stream, buffer) @test fetch(frame_count_1) == 48000 @test fetch(frame_count_2) == 48000 end sleep(1) end @testset "Constructors" begin PortAudioStream(2, maximum; adjust_channels = true) do stream @test isopen(stream) end PortAudioStream(output_name; adjust_channels = true) do stream @test isopen(stream) end PortAudioStream(input_name, output_name; adjust_channels = true) do stream @test isopen(stream) end end @testset "Errors with sound" begin big = typemax(Int) @test_throws DomainError( typemax(Int), "$big exceeds maximum output channels for $output_name", ) PortAudioStream(input_name, output_name, 0, big) @test_throws ArgumentError("Input or output must have at least 1 channel") PortAudioStream( input_name, output_name, 0, 0; adjust_channels = true, ) @test_throws ArgumentError(""" Default sample rate 0 for input \"$input_name\" disagrees with default sample rate 1 for output \"$output_name\". Please specify a sample rate. """) combine_default_sample_rates( get_device(input_name), 0, get_device(output_name), 1, ) end @testset "libportaudio with sound" begin @test PaErrorCode(Pa_HostApiTypeIdToHostApiIndex(paInDevelopment)) == paHostApiNotFound @test Pa_HostApiDeviceIndexToDeviceIndex(paInDevelopment, 0) == 0 stream = PortAudioStream(input_name, output_name, 2, 2; adjust_channels = true) pointer_to = stream.pointer_to @test handle_status(Pa_GetStreamReadAvailable(pointer_to)) >= 0 @test handle_status(Pa_GetStreamWriteAvailable(pointer_to)) >= 0 @test Bool(handle_status(Pa_IsStreamActive(pointer_to))) @test safe_load(Pa_GetStreamInfo(pointer_to), ErrorException("no info")) isa PaStreamInfo @test Pa_GetStreamTime(pointer_to) >= 0 @test Pa_GetStreamCpuLoad(pointer_to) >= 0 @test PaErrorCode(handle_status(Pa_AbortStream(pointer_to))) == paNoError @test PaErrorCode( handle_status(Pa_SetStreamFinishedCallback(pointer_to, C_NULL)), ) == paNoError end end doctest(PortAudio) end