def test_fft_ifft_split(graph): #-------------------------------------------------------------------------------- # Verify that fft -> ifft returns the same output, with output # buffer smaller than FFT size. #-------------------------------------------------------------------------------- buffer_a = Buffer(1, fft_size) buffer_b1 = Buffer(1, fft_size // 2) buffer_b2 = Buffer(1, fft_size // 2) buffer_b3 = Buffer(1, fft_size // 2) process_tree(SineOscillator(440), buffer_a) fft = FFT(SineOscillator(440), fft_size=fft_size, hop_size=fft_size, do_window=False) ifft = IFFT(fft) process_tree(ifft, buffer_b1) process_tree(ifft, buffer_b2) process_tree(ifft, buffer_b3) #-------------------------------------------------------------------------------- # Note that the first buffer will be empty as no output will be # generated until 1 fft_size worth of samples has been processed. #-------------------------------------------------------------------------------- buffer_b_concatenate = np.concatenate((buffer_b2.data[0], buffer_b3.data[0])) assert np.all(buffer_b1.data[0] == 0) assert np.all(np.abs(buffer_a.data[0] - buffer_b_concatenate) < 0.00001)
def test_fft_windowed(graph): #-------------------------------------------------------------------------------- # Verify that single-hop FFT output matches numpy's fft #-------------------------------------------------------------------------------- buffer_a = Buffer(1, fft_size) buffer_b = Buffer(1, num_bins * 2) process_tree(SineOscillator(440), buffer_a) # Modify to match the symmetry of vDSP FFT window = np.hanning(buffer_a.num_frames + 1)[:buffer_a.num_frames] windowed = buffer_a.data[0] * window spectrum = np.fft.rfft(windowed) mags_py = np.abs(spectrum)[:num_bins] angles_py = np.angle(spectrum)[:num_bins] fft = FFT(SineOscillator(440), fft_size=fft_size, hop_size=fft_size, do_window=True) process_tree(fft, buffer_b) # Apple vDSP applies a scaling factor of 2x after forward FFT mags_out = np.copy(buffer_b.data[0][:num_bins]) / 2 angles_out = np.copy(buffer_b.data[0][num_bins:]) # phases are mismatched for some reason assert np.all(np.abs(mags_py - mags_out) < 0.0001)
def test_expansion_buffer_reallocation(graph): a = SineOscillator([440] * 4) assert a.num_output_channels == 4 assert a.num_output_channels_allocated == 32 a.set_input("frequency", [440] * 100) assert a.num_output_channels == 100 assert a.num_output_channels_allocated == 100
def test_buffer_recorder(graph): record_buf = Buffer(2, 1024) sine = SineOscillator(440) recorder = BufferRecorder(record_buf, sine, loop=False) assert recorder.num_input_channels == 2 assert recorder.num_output_channels == 0 assert recorder.state == SIGNALFLOW_NODE_STATE_ACTIVE graph.add_node(recorder) graph.render(len(record_buf) + 1) assert recorder.state == SIGNALFLOW_NODE_STATE_STOPPED sine_rendered = np.sin( np.arange(len(record_buf)) * np.pi * 2 * 440 / graph.sample_rate) assert list(record_buf.data[0]) == pytest.approx(sine_rendered, abs=0.0001) assert list(record_buf.data[1]) == pytest.approx(sine_rendered, abs=0.0001) sine2 = SineOscillator(2000) recorder2 = BufferRecorder(record_buf, sine2, feedback=0.5, loop=True) assert recorder2.num_input_channels == 2 assert recorder2.num_output_channels == 0 assert recorder2.state == SIGNALFLOW_NODE_STATE_ACTIVE process_tree(recorder2, num_frames=len(record_buf)) assert recorder2.state == SIGNALFLOW_NODE_STATE_ACTIVE sine_rendered2 = 0.5 * sine_rendered + np.sin( np.arange(len(record_buf)) * np.pi * 2 * 2000 / graph.sample_rate) assert list(record_buf.data[0]) == pytest.approx(sine_rendered2, abs=0.001) sine3 = SineOscillator(2000) recorder2.set_input("input", sine3) process_tree(recorder2, num_frames=len(record_buf)) assert recorder2.state == SIGNALFLOW_NODE_STATE_ACTIVE sine_rendered2 = 0.5 * sine_rendered2 + np.sin( np.arange(len(record_buf)) * np.pi * 2 * 2000 / graph.sample_rate) assert list(record_buf.data[0]) == pytest.approx(sine_rendered2, abs=0.001)
def test_graph_render(): graph = AudioGraph() sine = SineOscillator(440) graph.play(sine) with pytest.raises(RuntimeError): graph.render(441000) del graph
def test_expansion_channel_mismatch(graph): a = SineOscillator([440, 880]) with pytest.raises(InvalidChannelCountException): b = LinearPanner(2, a) b = Buffer([1, 2, 3]) c = BufferPlayer(b) with pytest.raises(InvalidChannelCountException): c.set_input("rate", [1, 1.5])
def test_fft_ifft(graph): #-------------------------------------------------------------------------------- # Verify that fft -> ifft returns the same output. # Buffer must be at least fft_size or the results will be delayed until fft_size # samples are queued. #-------------------------------------------------------------------------------- for buffer_size in [ fft_size, fft_size * 2, fft_size * 4 ]: buffer_a = Buffer(1, buffer_size) buffer_b = Buffer(1, buffer_size) process_tree(SineOscillator(440), buffer_a) fft = FFT(SineOscillator(440), fft_size=fft_size, hop_size=fft_size, do_window=False) ifft = IFFT(fft) process_tree(ifft, buffer_b) assert np.all(np.abs(buffer_a.data[0] - buffer_b.data[0]) < 0.000001)
def test_fft_convolve(graph): if no_fft: return buffer_a = Buffer(1, fft_size) process_tree(Impulse(0), buffer_a) buffer_b = Buffer(1, fft_size) process_tree(SineOscillator(440), buffer_b) fft = FFT(SineOscillator(440), fft_size=fft_size, hop_size=fft_size, do_window=False) convolve = FFTConvolve(fft, buffer_a) ifft = IFFT(convolve) * 0.5 buffer_c = Buffer(1, fft_size) process_tree(ifft, buffer_c) assert np.all(np.abs(buffer_b.data[0] - buffer_c.data[0]) < 0.000001)
def test_patch_free(graph): prototype = Patch() sine = prototype.add_node(SineOscillator(440)) envelope = prototype.add_node(EnvelopeASR(0.0, 0.0, 0.01)) output = sine * envelope prototype.set_output(output) spec = prototype.create_spec() patch = Patch(spec) patch.auto_free = True
def test_expansion_channel_array(graph): a = SineOscillator(440) b = SineOscillator(880) c = SineOscillator([1760, 3520]) d = ChannelArray([a, b, c]) e = ChannelMixer(1, d) assert a.num_input_channels == a.num_output_channels == 1 assert b.num_input_channels == b.num_output_channels == 1 assert c.num_input_channels == c.num_output_channels == 2 assert d.num_input_channels == d.num_output_channels == 4 assert e.num_input_channels == 4 assert e.num_output_channels == 1 buf = Buffer(1, DEFAULT_BUFFER_LENGTH) process_tree(e, buffer=buf) peak_frequencies = get_peak_frequencies(buf.data[0], graph.sample_rate) assert np.all( peak_frequencies == pytest.approx([440, 880, 1760, 3520], abs=(graph.sample_rate / DEFAULT_BUFFER_LENGTH / 2)))
def test_graph_dummy_audioout(): output = AudioOut_Dummy(2) graph = AudioGraph(output_device=output) sine = SineOscillator([ 220, 440 ]) graph.play(sine) graph.render(1024) samples = graph.output.output_buffer[0][:1024] assert len(samples) == 1024 del graph
def test_nodes_oscillators_sine(graph): a = SineOscillator([10, 20]) process_tree(a, num_frames=N) #-------------------------------------------------------------------------------- # The output of SineOscillator() seems to diverge from np.sin() so this precision is # not very high. Should find out why at some point -- probably numerical # precision. #-------------------------------------------------------------------------------- expected = np.sin(np.arange(N) * np.pi * 2 * 10 / graph.sample_rate) assert list(a.output_buffer[0]) == pytest.approx(expected, abs=0.0001) expected = np.sin(np.arange(N) * np.pi * 2 * 20 / graph.sample_rate) assert list(a.output_buffer[1]) == pytest.approx(expected, abs=0.0001)
def test_graph_sample_rate(): graph = AudioGraph() assert graph.sample_rate > 0 graph.sample_rate = 1000 assert graph.sample_rate == 1000 with pytest.raises(Exception): graph.sample_rate = 0 buf = Buffer(1, 1000) sine = SineOscillator(10) process_tree(sine, buf) assert count_zero_crossings(buf.data[0]) == 10 del graph
def test_fft(graph): #-------------------------------------------------------------------------------- # Verify that single-hop FFT output matches numpy's fft # 1026 = 512 bins plus Nyquist #-------------------------------------------------------------------------------- buffer_a = Buffer(1, fft_size) buffer_b = Buffer(1, num_bins * 2) process_tree(SineOscillator(440), buffer_a) spectrum = np.fft.rfft(buffer_a.data[0]) mags_py = np.abs(spectrum)[:num_bins] angles_py = np.angle(spectrum)[:num_bins] fft = FFT(SineOscillator(440), fft_size=fft_size, hop_size=fft_size, do_window=False) process_tree(fft, buffer_b) # Apple vDSP applies a scaling factor of 2x after forward FFT # TODO: Fix this (and remove scaling factor in fftw forward fft) mags_out = np.copy(buffer_b.data[0][:num_bins]) / 2 angles_out = np.copy(buffer_b.data[0][num_bins:]) assert np.all(np.abs(mags_py - mags_out) < 0.001) assert np.all(np.abs(angles_py - angles_out) < 0.001)
def test_expansion_multi(graph): """ When passed an array as an argument, the input is automatically converted into a ChannelArray and the output number of channels should be increased. """ a = SineOscillator([0.0, 1.0]) assert a.num_output_channels == 2 assert a.num_input_channels == 2 frequency = a.inputs["frequency"] assert frequency.name == "channel-array" assert frequency.inputs["input0"].name == "constant" assert frequency.inputs["input1"].name == "constant" process_tree(a) assert np.all(frequency.inputs["input0"].output_buffer[0] == 0.0) assert np.all(frequency.inputs["input1"].output_buffer[0] == 1.0)
def test_fft_convolve_split(graph): if no_fft: return buffer_ir = Buffer(1, fft_size * 4) envelope_duration_seconds = buffer_ir.num_frames / graph.sample_rate envelope = EnvelopeASR(0, 0, envelope_duration_seconds) * SineOscillator(440) process_tree(envelope, buffer_ir) fft = FFT(Impulse(0), fft_size=fft_size, hop_size=fft_size, do_window=False) convolve = FFTConvolve(fft, buffer_ir) ifft = IFFT(convolve) * 0.5 buffer_out = Buffer(1, fft_size) output_samples = np.zeros(0) for n in range(4): process_tree(ifft, buffer_out) output_samples = np.concatenate((output_samples, buffer_out.data[0])) assert np.all(np.abs(output_samples - buffer_ir.data[0]) < 0.000001)
def test_expansion_many_channels(graph): """ Generate 100 sine channels, mix down to mono, and confirm that the correct 100 frequencies are present. By multiplying the Sine output by 0.5, we also test that the Multiply nodes re-allocates the output buffers of its inputs (the Constant(0.5)) for automatic upmixing by the AudioGraph. """ frequencies = 1000 + np.arange(100) * 100 a = SineOscillator(frequencies) * 0.5 mixer = ChannelMixer(1, a) graph.render_subgraph(mixer, 2048) peak_frequencies = get_peak_frequencies(mixer.output_buffer[0], graph.sample_rate) peak_frequencies_rounded = np.round(peak_frequencies, -2) assert np.array_equal(peak_frequencies_rounded, frequencies) assert a.num_output_channels_allocated == 100 assert a.output_buffer.shape == (100, 2048)
def test_expansion_recursive(graph): """ Check that num_output_channels values propagate through the graph. """ a = SineOscillator(440) b = BiquadFilter(a) buf = WaveShaperBuffer(256) buf.fill(lambda n: n**2) c = WaveShaper(b, buf) d = AllpassDelay(c) assert a.num_input_channels == a.num_output_channels == 1 assert b.num_input_channels == b.num_output_channels == 1 assert c.num_input_channels == c.num_output_channels == 1 assert d.num_input_channels == d.num_output_channels == 1 a.set_input("frequency", [440, 880, 1320]) assert a.num_input_channels == a.num_output_channels == 3 assert b.num_input_channels == b.num_output_channels == 3 assert c.num_input_channels == c.num_output_channels == 3 assert d.num_input_channels == d.num_output_channels == 3 a.set_input("frequency", 100) assert a.num_input_channels == a.num_output_channels == 1 assert b.num_input_channels == b.num_output_channels == 1 assert c.num_input_channels == c.num_output_channels == 1 assert d.num_input_channels == d.num_output_channels == 1
def test_node_no_graph(): with pytest.raises(Exception): a = SineOscillator(440)
def test_node_set_input(graph): a = SineOscillator(440) #-------------------------------------------------------------------------------- # Note that, when an input is set to a scalar value and the existing input # node is of type Constant, the value of the existing Constant is updated. # This means that we can re-use the same `frequency` reference below. #-------------------------------------------------------------------------------- frequency = a.get_input("frequency") process_tree(a, num_frames=1024) assert frequency.output_buffer[0][0] == 440 a.set_input("frequency", 880) process_tree(a, num_frames=1024) assert frequency.output_buffer[0][0] == 880 a.set_input("frequency", Line(0, 1, 1.0)) process_tree(a, num_frames=1024) line = a.get_input("frequency") assert line.output_buffer[0][0] == 0 assert line.output_buffer[0][1] > 0 #-------------------------------------------------------------------------------- # Check that replacing a non-Constant with a Constant works as expected. #-------------------------------------------------------------------------------- a.set_input("frequency", 1760) frequency = a.get_input("frequency") process_tree(a, num_frames=1024) assert frequency.output_buffer[0][0] == 1760
def test_node_process(graph): a = SineOscillator(440) a.process(1024) assert a.output_buffer.shape == (32, 1024)
def test_node_add_input(graph): a = SineOscillator(440) b = SineOscillator(440) with pytest.raises(RuntimeError): a.add_input(b)
def test_expansion_mono(graph): a = SineOscillator(1) assert a.num_output_channels == 1 assert a.num_input_channels == 1