# examp.rb -- something from examp.scm # Translator/Author: Michael Scholz # Created: 02/09/04 18:34:00 # Changed: 17/12/24 22:01:05 # module Examp (examp.scm) # selection_rms # region_rms(n) # window_samples(snd, chn) # display_energy(snd, chn) # display_db(snd, chn) # window_rms # fft_peak(snd, chn, scale) # finfo(file) # display_correlate(snd, chn, y0, y1) # # zoom_spectrum(snd, chn, y0, y1) # zoom_fft(snd, chn, y0, y1) # superimpose_ffts(snd, chn, y0, y1) # locate_zero(limit) # shell(cmd, *rest) # # mpg(mpgfile, rawfile) # read_ogg(filename) # write_ogg(snd) # read_speex(filename) # write_speex(snd) # read_flac(filename) # write_flac(snd) # read_ascii(in_filename, out_filename, out_type, out_format, out_srate) # auto_dot(snd, chn, y0, y1) # # first_mark_in_window_at_left # flash_selected_data(interval) # mark_loops # do_all_chans(origin) do |y| ... end # update_graphs # do_chans(*origin) do |y| ... end # do_sound_chans(*origin) do |y| ... end # every_sample? do |y| ... end # sort_samples(nbins) # place_sound(mono_snd, stereo_snd, pan_env) # # fft_edit(bottom, top, snd, chn) # fft_squelch(squelch, snd, chn) # fft_cancel(lo_freq, hi_freq, snd, chn) # # class Ramp < Musgen # initialize(size) # run_func(up, dummy) # run(up) # # make_ramp(size) # ramp(gen, up) # squelch_vowels(snd, chn) # fft_env_data(fft_env, snd, chn) # fft_env_edit(fft_env, snd, chn) # fft_env_interp(env1, env2, interp, snd, chn) # fft_smoother(cutoff, start, samps, snd, chn) # # comb_filter(scaler, size) # comb_chord(scaler, size, amp, interval_one, interval_two) # zcomb(scaler, size, pm) # notch_filter(scaler, size) # formant_filter(radius, freq) # formants(r1, f1, r2, f2, r3, f3) # moving_formant(radius, move) # osc_formants(radius, bases, amounts, freqs) # echo(scaler, secs) # zecho(scaler, secs, freq, amp) # flecho(scaler, secs) # ring_mod(freq, gliss_env) # am(freq) # vibro(speed, depth) # hello_dentist(freq, amp, snd, chn) # fp(sr, osamp, osfreq, snd, chn) # compand() # expsrc(rate, snd, chn) # expsnd(gr_env, snd, chn) # cross_synthesis(cross_snd, amp, fftsize, r) # voiced2unvoiced(amp, fftsize, r, temp, snd, chn) # pulse_voice(cosin, freq, amp, fftsize, r, snd, chn) # cnvtest(snd0, snd1, amp) # # swap_selection_channels # make_sound_interp(start, snd, chn) # sound_interp(func, loc) # sound_via_sound(snd1, snd2) # env_sound_interp(envelope, time_scale, snd, chn) # granulated_sound_interp(en, time_scale, grain_len, grain_env, out_hop, s, c) # filtered_env(en, snd, chn) # # class Mouse # initialize # press(snd, chn, button, state, x, y) # drag(snd, chn, button, state, x, y) # # files_popup_buffer(type, position, name) # # find_click(loc) # remove_clicks # search_for_click # zero_plus # next_peak # find_pitch(pitch) # file2vct(file) # add_notes(notes, snd, chn) # region_play_list(data) # region_play_sequence(data) # replace_with_selection # explode_sf2 # # class Next_file # initialize # open_next_file_in_directory # click_middle_button_to_open_next_file_in_directory # # chain_dsps(start, dur, *dsps) # # scramble_channels(*new_order) # scramble_channel(silence) # # reverse_by_blocks(block_len, snd, chn) # reverse_within_blocks(block_len, snd, chn) # sound2segment_data(main_dir, output_file) # channel_clipped?(snd, chn) # scan_sound(func, beg, dur, snd) # or scan_sound_rb(beg, dur, snd) do |y, chn| ... end # # class Moog_filter < Musgen (moog.scm) # initialize(freq, q) # frequency=(freq) # filter(insig) # # module Moog # make_moog_filter(freq, q) # moog_filter(moog, insig) # moog(freq, q) require "clm" module Examp # (ext)snd.html examples made harder to break # # this mainly involves keeping track of the current sound/channel add_help(:selection_rms, "selection_rms() \ Returns rms of selection data using samplers.") def selection_rms if selection? reader = make_sampler(selection_position, false, false) len = selection_framples() sum = 0.0 len.times do val = next_sample(reader) sum = sum + val * val end free_sampler(reader) sqrt(sum / len) else Snd.raise(:no_active_selection) end end add_help(:region_rms, "region_rms(n=0) \ Returns rms of region N's data (chan 0).") def region_rms(n = 0) if region?(n) data = region2vct(n, 0, 0) sqrt(dot_product(data, data) / data.length) else Snd.raise(:no_such_region) end end add_help(:window_samples, "window_samples(snd=false, chn=false) \ Returns samples in snd channel chn in current graph window.") def window_samples(snd = false, chn = false) wl = left_sample(snd, chn) wr = right_sample(snd, chn) channel2vct(wl, 1 + (wr - wl), snd, chn) end add_help(:display_energy, "display_energy(snd, chn) \ Is a $lisp_graph_hook function to display the time domain \ data as energy (squared): $lisp_graph_hook.add_hook!(\"display-energy\", \ &method(:display_energy).to_proc)") def display_energy(snd, chn) ls = left_sample(snd, chn) rs = right_sample(snd, chn) datal = make_graph_data(snd, chn) data = vct?(datal) ? datal : datal[1] sr = srate(snd) y_max = y_zoom_slider(snd, chn) if data and ls and rs vct_multiply!(data, data) graph(data, "energy", ls / sr, rs / sr, 0.0, y_max * y_max, snd, chn) end end add_help(:display_db, "display_db(snd, chn) \ Is a $lisp_graph_hook function to display the time domain data in dB: $lisp_graph_hook.add_hook!(\"display-db\", &method(:display_db).to_proc)") def display_db(snd, chn) if datal = make_graph_data(snd, chn) dB = lambda do |val| if val < 0.001 -60.0 else 20.0 * log10(val) end end data = vct?(datal) ? datal : datal[1] sr = srate(snd) ls = left_sample(snd, chn) rs = right_sample(snd, chn) data.map! do |val| 60.0 + dB.call(val.abs) end graph(data, "dB", ls / sr, rs / sr, 0.0, 60.0, snd, chn) end end add_help(:window_rms, "window_rms() \ Returns rms of data in currently selected graph window.") def window_rms ls = left_sample rs = right_sample data = channel2vct(ls, 1 + (rs - ls)) sqrt(dot_product(data, data), data.length) end add_help(:fft_peak, "fft_peak(snd, chn, scale) \ Returns the peak spectral magnitude: $after_transform_hook.add_hook!(\"fft-peak\") do |snd, chn, scale| fft_peak(snd, chn, scale) end") def fft_peak(snd, chn, scale) if transform_graph? and transform_graph_type == Graph_once pk = (2.0 * vct_peak(transform2vct(snd, chn))) / transform_size status_report(pk.to_s, snd) pk else false end end # 'info' from extsnd.html using format add_help(:finfo, "finfo(file) \ Returns description (as a string) of FILE.") def finfo(file) chans = mus_sound_chans(file) sr = mus_sound_srate(file) format("%s: chans: %d, srate: %d, %s, %s, len: %1.3f", file, chans, sr, mus_header_type_name(mus_sound_header_type(file)), mus_sample_type_name(mus_sound_sample_type(file)), mus_sound_samples(file).to_f / (chans * sr.to_f)) end # Correlation # # correlation of channels in a stereo sound add_help(:display_correlate, "display_correlate(snd, chn, y0, y1) \ Returns the correlation of SND's 2 channels (intended for use with $graph_hook): $graph_hook.add_hook!(\"display_correlate\") do |snd, chn, y0, y1| display_correlate(snd, chn, y0, y1) end") def display_correlate(snd, chn, y0, y1) if channels(snd) == 2 and framples(snd, 0) > 1 and framples(snd, 1) > 1 ls = left_sample(snd, 0) rs = right_sample(snd, 0) ilen = 1 + (rs - ls) pow2 = (log(ilen) / log(2)).ceil fftlen = (2 ** pow2).to_i fftlen2 = fftlen / 2 fftscale = 1.0 / fftlen rl1 = channel2vct(ls, fftlen, snd, 0) rl2 = channel2vct(ls, fftlen, snd, 1) im1 = make_vct(fftlen) im2 = make_vct(fftlen) fft(rl1, im1, 1) fft(rl2, im2, 1) tmprl = vct_copy(rl1) tmpim = vct_copy(im1) data3 = make_vct(fftlen2) vct_multiply!(tmprl, rl2) vct_multiply!(tmpim, im2) vct_multiply!(im2, rl1) vct_multiply!(rl2, im1) vct_add!(tmprl, tmpim) vct_subtract!(im2, rl2) fft(tmprl, im2, -1) vct_add!(data3, tmprl) vct_scale!(data3, fftscale) graph(data3, "lag time", 0, fftlen2) else snd_print("correlate wants stereo input") end end # set transform-size based on current time domain window size # # also zoom spectrum based on y-axis zoom slider add_help(:zoom_spectrum, "zoom_spectrum(snd, chn, y0, y1) \ Sets the transform size to correspond to the \ time-domain window size (use with $graph_hook): $graph_hook.add_hook!(\"zoom-spectrum\") do |snd, chn, y0, y1| zoom_spectrum(snd, chn, y0, y1) end") def zoom_spectrum(snd, chn, y0, y1) if transform_graph?(snd, chn) and transform_graph_type(snd, chn) == Graph_once set_transform_size((2 ** (log(right_sample(snd, chn) - left_sample(snd, chn))/log(2.0))).to_i, snd, chn) set_spectrum_end(y_zoom_slider(snd, chn), snd, chn) end false end add_help(:zoom_fft, "zoom_fft(snd, chn, y0, y1) \ Sets the transform size if the time domain is \ not displayed (use with $graph_hook). \ It also sets the spectrum display start point \ based on the x position slider---this can be confusing \ if fft normalization is on (the default): $graph_hook.add_hook!(\"zoom-fft\") do |snd, chn, y0, y1| zoom_fft(snd, chn, y0, y1) end") def zoom_fft(snd, chn, y0, y1) if transform_graph?(snd, chn) and (not time_graph?(snd, chn)) and transform_graph_type(snd, chn) == Graph_once set_transform_size(2 ** (log(right_sample(snd, chn) - left_sample(snd, chn)) / log(2.0)).ceil, snd, chn) set_spectrum_start(x_position_slider(snd, chn), snd, chn) set_spectrum_end(y_zoom_slider(snd, chn), snd, chn) end false end # superimpose spectra of sycn'd sounds add_help(:superimpose_ffts, "superimpose_ffts(snd, chn, y0, y1) \ Superimposes ffts of multiple (syncd) sounds (use with $graph_hook): $graph_hook.add_hook!(\"superimpose-ffts\") do |snd, chn, y0, y1| superimpose_ffts(snd, chn, y0, y1) end") def superimpose_ffts(snd, chn, y0, y1) maxsync = Snd.sounds.map do |s| sync(s) end.max if sync(snd) > 0 and snd == Snd.sounds.map do |s| sync(snd) == sync(s) ? sound2integer(s) : (maxsync + 1) end.min ls = left_sample(snd, chn) rs = right_sample(snd, chn) pow2 = (log(rs - ls) / log(2)).ceil fftlen = (2 ** pow2).to_i if pow2 > 2 ffts = [] Snd.sounds.each do |s| if sync(snd) == sync(s) and channels(s) > chn fdr = channel2vct(ls, fftlen, s, chn) fdi = make_vct(fftlen) spectr = make_vct(fftlen / 2) ffts.push(spectr.add(spectrum(fdr, fdi, false, 2))) end end graph(ffts, "spectra", 0.0, 0.5, y0, y1, snd, chn) end end false end # c-g? example (Anders Vinjar) add_help(:locate_zero, "locate_zero(limit) \ Looks for successive samples that sum to less than LIMIT, \ moving the cursor if successful.") def locate_zero(limit) start = cursor sf = make_sampler(start, false, false) val0 = sf.call.abs val1 = sf.call.abs n = start until sampler_at_end?(sf) or val0 + val1 < limit val0, val1 = val1, sf.call.abs n += 1 end free_sampler(sf) set_cursor(n) end # make a system call from irb or snd listener # # shell("df") for example # or to play a sound whenever a file is closed: # $close-hook.add_hook!() do |snd| shell("sndplay wood16.wav"); false end add_help(:shell, "shell(cmd, *rest) \ Sends CMD to a shell (executes it as a shell command) \ and returns the result string.") def shell(cmd, *rest) str = "" unless cmd.null? IO.popen(format(cmd, *rest), "r") do |f| str = f.readlines.join end end str end # translate mpeg input to 16-bit linear and read into Snd # # mpg123 with the -s switch sends the 16-bit (mono or stereo) representation # of an mpeg file to stdout. There's also apparently a switch to write # 'wave' output. add_help(:mpg, "mpg(file, tmpname) \ Converts file from MPEG to raw 16-bit samples using mpg123: \ mpg(\"mpeg.mpg\", \"mpeg.raw\")") def mpg(mpgfile, rawfile) b0 = b1 = b2 = b3 = 0 File.open(mpgfile, "r") do |fd| b0 = fd.readchar b1 = fd.readchar b2 = fd.readchar b3 = fd.readchar end if b0 != 255 or (b1 & 0b11100000) != 0b11100000 Snd.display("%s is not an MPEG file (first 11 bytes: %b %b", mpgfile.inspect, b0, b1 & 0b11100000) else id = (b1 & 0b11000) >> 3 layer = (b1 & 0b110) >> 1 srate_index = (b2 & 0b1100) >> 2 channel_mode = (b3 & 0b11000000) >> 6 if id == 1 Snd.display("odd: %s is using a reserved Version ID", mpgfile) end if layer == 0 Snd.display("odd: %s is using a reserved layer description", mpgfile) end chans = channel_mode == 3 ? 1 : 2 mpegnum = id.zero? ? 4 : (id == 2 ? 2 : 1) mpeg_layer = layer == 3 ? 1 : (layer == 2 ? 2 : 3) srate = [44100, 48000, 32000, 0][srate_index] / mpegnum Snd.display("%s: %s Hz, %s, MPEG-%s", mpgfile, srate, chans == 1 ? "mono": "stereo", mpeg_layer) system(format("mpg123 -s %s > %s", mpgfile, rawfile)) open_raw_sound(rawfile, chans, srate, little_endian? ? Mus_lshort : Mus_bshort) end end # read and write OGG files add_help(:read_ogg, "read_ogg(filename) \ Read OGG files: $open_hook.add_hook!(\"read-ogg\") do |filename| if mus_sound_header_type(filename) == Mus_raw read_ogg(filename) else false end end") def read_ogg(filename) flag = false File.open(filename, "r") do |fd| flag = fd.readchar == ?O and fd.readchar == ?g and fd.readchar == ?g and fd.readchar == ?S end if flag aufile = filename + ".au" File.unlink(aufile) if File.exist?(aufile) system(format("ogg123 -d au -f %s %s", aufile, filename)) aufile else false end end def write_ogg(snd) if edits(snd)[0] > 0 or header_type(snd) != Mus_riff file = file_name(snd) + ".tmp" save_sound_as(file, snd, :header_type, Mus_riff) system("oggenc " + file) File.unlink(file) else system("oggenc " + file_name(snd)) end end # read and write Speex files def read_speex(filename) wavfile = filename + ".wav" File.unlink(wavfile) if File.exist?(wavfile) system(format("speexdec %s %s", filename, wavfile)) wavfile end def write_speex(snd) if edits(snd)[0] > 0 or header_type(snd) != Mus_riff file = file_name(snd) + ".wav" spxfile = file_name(snd) + "spx" save_sound_as(file, snd, :header_type, Mus_riff) system(format("speexenc %s %s", file, spxfile)) File.unlink(file) else system(format("speexenc %s %s", file_name(snd), spxfile)) end end # read and write FLAC files def read_flac(filename) system(format("flac -d %s", filename)) end def write_flac(snd) if edits(snd)[0] > 0 or header_type(snd) != Mus_riff file = file_name(snd) + ".wav" save_sound_as(file, snd, :header_type, Mus_riff) system(format("flac %s", file)) File.unlink(file) else system(format("flac %s ", file_name(snd))) end end # read ASCII files # # these are used by Octave (WaveLab) -- each line has one integer, # apparently a signed short. def read_ascii(in_filename, out_filename = "test.snd", out_type = Mus_next, out_format = Mus_bshort, out_srate = 44100) in_buffer = IO.readlines(in_filename) # array of strings com = format("created by %s: %s", get_func_name, in_filename) out_snd = new_sound(out_filename, 1, out_srate, out_format, out_type, com) bufsize = 512 data = make_vct(bufsize) loc = 0 frame = 0 short2float = 1.0 / 32768.0 as_one_edit_rb do | | in_buffer.each do |line| line.split.each do |str_val| val = eval(str_val) data[loc] = val * short2float loc += 1 if loc == bufsize vct2channel(data, frame, bufsize, out_snd, 0) frame += bufsize loc = 0 end end end if loc > 0 vct2channel(data, frame, loc, out_snd, 0) end end out_snd end # make dot size dependent on number of samples being displayed # # this could be extended to set time_graph_style to Graph_lines if # many samples are displayed, etc add_help(:auto_dot, "auto_dot(snd, chn, y0, y1) \ Sets the dot size depending on the number \ of samples being displayed (use with $graph_hook): $graph_hook.add_hook!(\"auto-dot\") do |snd, chn, y0, y1| auto_dot(snd, chn, y0, y1) end") def auto_dot(snd, chn, y0, y1) dots = right_sample(snd, chn) - left_sample(snd, chn) case dots when 100 set_dot_size(1, snd, chn) when 50 set_dot_size(2, snd, chn) when 25 set_dot_size(3, snd, chn) else set_dot_size(5, snd, chn) end false end # move window left edge to mark upon 'm' # # in large sounds, it can be pain to get the left edge of the window # aligned with a specific spot in the sound. In this code, we # assume the desired left edge has a mark, and the 'm' key (without # control) will move the window left edge to that mark. add_help(:first_mark_in_window_at_left, "first_mark_in_window_at_left() \ Moves the graph so that the leftmost visible mark is at the left edge: bind_key(?m, 0, lambda do | | first_mark_in_window_at_left end)") def first_mark_in_window_at_left keysnd = Snd.snd keychn = Snd.chn current_left_sample = left_sample(keysnd, keychn) chan_marks = marks(keysnd, keychn) if chan_marks.null? snd_print("no marks!") else leftmost = chan_marks.map do |m| mark_sample(m) end.detect do |m| m > current_left_sample end if leftmost.null? snd_print("no mark in window") else set_left_sample(leftmost, keysnd, keychn) Keyboard_no_action end end end # flash selected data red and green add_help(:flash_selected_data, "flash_selected_data(millisecs) \ Causes the selected data to flash red and green.") def flash_selected_data(interval) if selected_sound set_selected_data_color(selected_data_color == Red ? Green : Red) call_in(interval, lambda do | | flash_selected_data(interval) end) end end # use loop info (if any) to set marks at loop points add_help(:mark_loops, "mark_loops() \ Places marks at loop points found in the selected sound's header.") def mark_loops loops = (sound_loop_info or mus_sound_loop_info(file_name)) if loops and !loops.empty? unless loops[0].zero? and loops[1].zero? add_mark(loops[0]) add_mark(loops[1]) unless loops[2].zero? and loops[3].zero? add_mark(loops[2]) add_mark(loops[3]) end end else Snd.display("%s has no loop info", short_file_name.inspect) end end # mapping extensions (map arbitrary single-channel function over # various channel collections) add_help(:do_all_chans, "do_all_chans(edhist) do |y| ... end \ Applies func to all active channels, \ using EDHIST as the edit history indication: \ do_all_chans(\"double all samples\", do |val| 2.0 * val end)") def do_all_chans(origin, &func) Snd.sounds.each do |snd| channels(snd).times do |chn| map_channel(func, 0, false, snd, chn, false, origin) end end end add_help(:update_graphs, "update_graphs() \ Updates (redraws) all graphs.") def update_graphs Snd.sounds.each do |snd| channels(snd).times do |chn| update_time_graph(snd, chn) end end end add_help(:do_chans, "do_chans(edhist) do |y| ... end \ Applies func to all sync'd channels using EDHIST \ as the edit history indication.") def do_chans(*origin, &func) snc = sync if snc > 0 Snd.sounds.each do |snd| channels(snd).times do |chn| if sync(snd) == snc map_channel(func, 0, false, snd, chn, false, (origin.empty? ? "" : format(*origin))) end end end else snd_warning("sync not set") end end add_help(:do_sound_chans, "do_sound_chans(edhist) do |y| ... end \ Applies func to all selected channels using EDHIST \ as the edit history indication.") def do_sound_chans(*origin, &func) if snd = selected_sound channels(snd).times do |chn| map_channel(func, 0, false, snd, chn, false, (origin.empty? ? "" : format(*origin))) end else snd_warning("no selected sound") end end add_help(:every_sample?, "every_sample? do |y| ... end \ Returns true if func is not false for all samples in the current channel, \ otherwise it moves the cursor to the first offending sample.") def every_sample?(&func) snd = Snd.snd chn = Snd.chn if baddy = scan_channel(lambda do |y| (not func.call(y)) end, 0, framples(snd, chn), snd, chn) set_cursor(baddy[1]) end (not baddy) end add_help(:sort_samples, "sort_samples(bins) \ Provides a histogram in BINS bins.") def sort_samples(nbins) bins = make_array(nbins, 0) scan_channel(lambda do |y| bin = (y.abs * nbins).floor bins[bin] += 1 false end) bins end # mix mono sound into stereo sound panning according to env add_help(:place_sound, "place_sound(mono_snd, stereo_snd, pan_env) \ Mixes a mono sound into a stereo sound, splitting it into two copies \ whose amplitudes depend on the envelope PAN_ENV. \ If PAN_ENV is a number, \ the sound is split such that 0 is all in channel 0 \ and 90 is all in channel 1.") def place_sound(mono_snd, stereo_snd, pan_env) len = framples(mono_snd) if number?(pan_env) pos = pan_env / 90.0 rd0 = make_sampler(0, mono_snd, false) rd1 = make_sampler(0, mono_snd, false) map_channel(lambda do |y| y + pos * read_sample(rd1) end, 0, len, stereo_snd, 1) map_channel(lambda do |y| y + (1.0 - pos) * read_sample(rd0) end, 0, len, stereo_snd, 0) else e0 = make_env(:envelope, pan_env, :length, len) e1 = make_env(:envelope, pan_env, :length, len) rd0 = make_sampler(0, mono_snd, false) rd1 = make_sampler(0, mono_snd, false) map_channel(lambda do |y| y + env(e1) * read_sample(rd1) end, 0, len, stereo_snd, 1) map_channel(lambda do |y| y + (1.0 - env(e0)) * read_sample(rd0) end, 0, len, stereo_snd, 0) end end # FFT-based editing add_help(:fft_edit, "fft_edit(low_Hz, high_Hz) \ Ffts an entire sound, removes all energy below low-Hz and all above high-Hz, \ then inverse ffts.") def fft_edit(bottom, top, snd = false, chn = false) sr = srate(snd).to_f len = framples(snd, chn) fsize = (2.0 ** (log(len) / log(2.0)).ceil).to_i rdata = channel2vct(0, fsize, snd, chn) idata = make_vct(fsize) lo = (bottom / (sr / fsize)).round hi = (top / (sr / fsize)).round fft(rdata, idata, 1) j = fsize - 1 lo.times do |i| rdata[i] = rdata[j] = 0.0 rdata[i] = rdata[j] = 0.0 j -= 1 end j = fsize - hi (hi..(fsize / 2)).each do |i| rdata[i] = rdata[j] = 0.0 rdata[i] = rdata[j] = 0.0 j -= 1 end fft(rdata, idata, -1) vct_scale!(rdata, 1.0 / fsize) vct2channel(rdata, 0, len - 1, snd, chn, false, format("%s(%s, %s", get_func_name, bottom, top)) end add_help(:fft_squelch, "fft_squelch(squelch, snd=false, chn=false) \ Ffts an entire sound, sets all bins to 0.0 whose energy is below squelch, \ then inverse ffts.") def fft_squelch(squelch, snd = false, chn = false) len = framples(snd, chn) fsize = (2.0 ** (log(len) / log(2.0)).ceil).to_i rdata = channel2vct(0, fsize, snd, chn) idata = make_vct(fsize) fsize2 = fsize / 2 fft(rdata, idata, 1) vr = vct_copy(rdata) vi = vct_copy(idata) rectangular2polar(vr, vi) scaler = vct_peak(vr) scl_squelch = squelch * scaler j = fsize - 1 fsize2.times do |i| if sqrt(rdata[i] * rdata[i] + idata[i] * idata[i]) < scl_squelch rdata[i] = rdata[j] = 0.0 rdata[i] = rdata[j] = 0.0 end j -= 1 end fft(rdata, idata, -1) vct_scale!(rdata, 1.0 / fsize) vct2channel(rdata, 0, len - 1, snd, chn, false, format("%s(%s", get_func_name, squelch)) scaler end add_help(:fft_cancel, "fft_cancel(lo_freq, hi_freq, snd=false, chn=false) \ Ffts an entire sound, sets the bin(s) representing lo_freq to hi_freq to 0.0, \ then inverse ffts.") def fft_cancel(lo_freq, hi_freq, snd = false, chn = false) sr = srate(snd).to_f len = framples(snd, chn) fsize = (2.0 ** (log(len) / log(2.0)).ceil).to_i rdata = channel2vct(0, fsize, snd, chn) idata = make_vct(fsize) fsize2 = fsize / 2 fft(rdata, idata, 1) hz_bin = sr / fsize lo_bin = (lo_freq / hz_bin).round hi_bin = (hi_freq / hz_bin).round j = fsize - lo_bin - 1 fsize2.times do |i| if i > hi_bin rdata[i] = rdata[j] = 0.0 rdata[i] = rdata[j] = 0.0 end j -= 1 end fft(rdata, idata, -1) vct_scale!(rdata, 1.0 / fsize) vct2channel(rdata, 0, len - 1, snd, chn, false, format("%s(%s, %s", get_func_name, lo_freq, hi_freq)) end # same idea but used to distinguish vowels (steady-state) from consonants class Ramp < Musgen def initialize(size = 128) super() @val = 0.0 @size = size @incr = 1.0 / size @up = true end def inspect format("%s.new(%d)", self.class, @size) end def to_s format("#<%s size: %d, val: %1.3f, incr: %1.3f, up: %s>", self.class, @size, @val, @incr, @up) end def run_func(up = true, dummy = false) @up = up @val += (@up ? @incr : -@incr) @val = [1.0, [0.0, @val].max].min end def run(up = true) self.run_func(up, false) end end add_help(:make_ramp, "make_ramp(size=128) \ Returns a ramp generator.") def make_ramp(size = 128) Ramp.new(size) end add_help(:ramp, "ramp(gen, up) \ Is a kind of CLM generator that produces a ramp of a given length, \ then sticks at 0.0 or 1.0 until the UP argument changes.") def ramp(gen, up) gen.run_func(up, false) end add_help(:squelch_vowels, "squelch_vowels(snd=false, chn=false) \ Suppresses portions of a sound that look like steady-state.") def squelch_vowels(snd = false, chn = false) fft_size = 32 fft_mid = (fft_size / 2.0).floor ramper = Ramp.new(256) peak = maxamp(snd, chn) / fft_mid read_ahead = make_sampler(0, snd, chn) rl = Vct.new(fft_size) do read_sample(read_ahead) end im = Vct.new(fft_size, 0.0) ctr = fft_size - 1 in_vowel = false map_channel(lambda do |y| ctr += 1 if ctr == fft_size ctr = 0 fft(rl, im, 1) rl.multiply!(rl) im.multiply!(im) rl.add!(im) im.fill(0.0) in_vowel = (rl[0] + rl[1] + rl[2] + rl[3]) > peak rl.map! do read_sample(read_ahead) end end y * (1.0 - ramper.run(in_vowel)) end, 0, false, snd, chn, false, "squelch_vowels(") end add_help(:fft_env_data, "fft_env_data(fft-env, snd=false, chn=false) \ Applies fft_env as spectral env to current sound, returning vct of new data.") def fft_env_data(fft_env, snd = false, chn = false) len = framples(snd, chn) fsize = (2 ** (log(len) / log(2.0)).ceil).to_i rdata = channel2vct(0, fsize, snd, chn) idata = make_vct(fsize) fsize2 = fsize / 2 en = make_env(:envelope, fft_env, :length, fsize2) fft(rdata, idata, 1) j = fsize - 1 fsize2.times do |i| val = env(en) rdata[i] *= val idata[i] *= val rdata[j] *= val idata[j] *= val j -= 1 end fft(rdata, idata, -1) vct_scale!(rdata, 1.0 / fsize) end add_help(:fft_env_edit, "fft_env_edit(fft-env, snd=false, chn=false) \ Edits (filters) current chan using fft_env.") def fft_env_edit(fft_env, snd = false, chn = false) vct2channel(fft_env_data(fft_env, snd, chn), 0, framples(snd, chn) - 1, snd, chn, false, format("%s(%s", get_func_name, fft_env.inspect)) end add_help(:fft_env_interp, "fft_env_interp(env1, env2, interp, snd=false, chn=false) \ Interpolates between two fft-filtered \ versions (env1 and env2 are the spectral envelopes) \ following interp (an env between 0 and 1).") def fft_env_interp(env1, env2, interp, snd = false, chn = false) data1 = fft_env_data(env1, snd, chn) data2 = fft_env_data(env2, snd, chn) len = framples(snd, chn) en = make_env(:envelope, interp, :length, len) new_data = make_vct!(len) do |i| pan = env(en) (1.0 - pan) * data1[i] + pan * data2[i] end vct2channel(new_data, 0, len - 1, snd, chn, false, format("%s(%s, %s, %s", get_func_name, env1.inspect, env2.inspect, interp.inspect)) end add_help(:fft_smoother, "fft_smoother(cutoff, start, samps, snd=false, chn=false) \ Uses fft-filtering to smooth a section: \ vct2channel(fft_smoother(0.1, cursor, 400, 0, 0), cursor, 400)") def fft_smoother(cutoff, start, samps, snd = false, chn = false) fftpts = (2 ** (log(samps + 1) / log(2.0)).ceil).to_i rl = channel2vct(start, samps, snd, chn) im = make_vct(fftpts) top = (fftpts * cutoff.to_f).floor old0 = rl[0] old1 = rl[samps - 1] oldmax = vct_peak(rl) fft(rl, im, 1) (top...fftpts).each do |i| rl[i] = im[i] = 0.0 end fft(rl, im, -1) vct_scale!(rl, 1.0 / fftpts) newmax = vct_peak(rl) if newmax.zero? rl else if (scl = oldmax / newmax) > 1.5 vct_scale!(rl, scl) end new0 = rl[0] new1 = rl[samps - 1] offset0 = old0 - new0 offset1 = old1 - new1 incr = offset0 == offset1 ? 0.0 : ((offset1 - offset0) / samps) trend = offset0 - incr rl.map do |val| trend += incr val += trend end end end # comb-filter add_help(:comb_filter, "comb_filter(scaler, size) \ Returns a comb-filter ready for map_channel etc: \ map_channel(comb_filter(0.8, 32)) \ If you're in a hurry use: clm_channel(make_comb(0.8, 32)) instead.") def comb_filter(scaler, size) cmb = make_comb(scaler, size) lambda do |inval| comb(cmb, inval) end end # by using filters at harmonically related sizes, we can get chords: add_help(:comb_chord, "comb_chord(scl, size, amp, interval_one=0.75, interval_two=1.2) \ Returns a set of harmonically-related comb filters: \ map_channel(comb_chord(0.95, 100, 0.3))") def comb_chord(scaler, size, amp, interval_one = 0.75, interval_two = 1.2) c1 = make_comb(scaler, size.to_i) c2 = make_comb(scaler, (interval_one * size).to_i) c3 = make_comb(scaler, (interval_two * size).to_i) lambda do |inval| amp * (comb(c1, inval) + comb(c2, inval) + comb(c3, inval)) end end # or change the comb length via an envelope: add_help(:zcomb, "zcomb(scaler, size, pm) \ Returns a comb filter whose length varies according to an envelope: \ map_channel(zcomb(0.8, 32, [0, 0, 1, 10]))") def zcomb(scaler, size, pm) max_envelope_1 = lambda do |en, mx| 1.step(en.length - 1, 2) do |i| mx = [mx, en[i]].max.to_f end mx end cmb = make_comb(scaler, size, :max_size, (max_envelope_1.call(pm, 0.0) + size + 1).to_i) penv = make_env(:envelope, pm, :length, framples()) lambda do |inval| comb(cmb, inval, env(penv)) end end add_help(:notch_filter, "notch_filter(scaler, size) \ Returns a notch-filter: map_channel(notch_filter(0.8, 32))") def notch_filter(scaler, size) gen = make_notch(scaler, size) lambda do |inval| notch(gen, inval) end end add_help(:formant_filter, "formant_filter(radius, frequency) \ Returns a formant generator: map_channel(formant_filter(0.99, 2400)) \ Faster is: filter_sound(make_formant(2400, 0.99))") def formant_filter(radius, freq) frm = make_formant(radius, freq) lambda do |inval| formant(frm, inval) end end # to impose several formants, just add them in parallel: add_help(:formants, "formants(r1, f1, r2, f2, r3, f3) \ Returns 3 formant filters in parallel: \ map_channel(formants(0.99, 900, 0.98, 1800, 0.99 2700))") def formants(r1, f1, r2, f2, r3, f3) fr1 = make_formant(f1, r1) fr2 = make_formant(f2, r2) fr3 = make_formant(f3, r3) lambda do |inval| formant(fr1, inval) + formant(fr2, inval) + formant(fr3, inval) end end add_help(:moving_formant, "moving_formant(radius, move) \ Returns a time-varying (in frequency) formant filter: \ map_channel(moving_formant(0.99, [0, 1200, 1, 2400]))") def moving_formant(radius, move) frm = make_formant(move[1], radius) menv = make_env(:envelope, move, :length, framples()) lambda do |inval| val = formant(frm, inval) frm.frequency = env(menv) val end end add_help(:osc_formants, "osc_formants(radius, bases, amounts, freqs) \ Set up any number of independently oscillating formants, \ then calls map_channel: \ osc_formants(0.99, vct(400, 800, 1200), vct(400, 800, 1200), vct(4, 2, 3))") def osc_formants(radius, bases, amounts, freqs) len = bases.length frms = make_array(len) do |i| make_formant(bases[i], radius) end oscs = make_array(len) do |i| make_oscil(freqs[i]) end map_channel_rb() do |x| val = 0.0 frms.each_with_index do |frm, i| val += formant(frm, x) set_mus_frequency(frm, bases[i] + amounts[i] * oscil(oscs[i])) end val end end # echo add_help(:echo, "echo(scaler, secs) \ Returns an echo maker: map_channel(echo(0.5, 0.5), 0 44100)") def echo(scaler, secs) del = make_delay((secs * srate()).round) lambda do |inval| inval + delay(del, scaler * (tap(del) + inval)) end end add_help(:zecho, "zecho(scaler, secs, freq, amp) \ Returns a modulated echo maker: \ map_channel(zecho(0.5, 0.75, 6, 10.0), 0, 65000)") def zecho(scaler, secs, freq, amp) os = make_oscil(freq) len = (secs * srate()).round del = make_delay(len, :max_size, (len + amp + 1).to_i) lambda do |inval| inval + delay(del, scaler * (tap(del) + inval), amp * oscil(os)) end end add_help(:flecho, "flecho(scaler, secs) \ Returns a low-pass filtered echo maker: \ map_channel(flecho(0.5, 0.9), 0, 75000)" ) def flecho(scaler, secs) flt = make_fir_filter(:order, 4, :xcoeffs, vct(0.125, 0.25, 0.25, 0.125)) del = make_delay((secs * srate()).round) lambda do |inval| inval + delay(del, fir_filter(flt, scaler * (tap(del) + inval))) end end # ring-mod and am # # CLM instrument is ring-modulate.ins add_help(:ring_mod, "ring_mod(freq, gliss_env) \ Returns a time-varying ring-modulation filter: \ map_channel(ring_mod(10, [0, 0, 1, hz2radians(100)]))") def ring_mod(freq, gliss_env) os = make_oscil(:frequency, freq) len = framples() genv = make_env(:envelope, gliss_env, :length, len) lambda do |inval| oscil(os, env(genv)) * inval end end add_help(:am, "am(freq) \ returns an amplitude-modulator: map_channel(am(440))") def am(freq) os = make_oscil(freq) lambda do |inval| amplitude_modulate(1.0, inval, oscil(os)) end end def vibro(speed, depth) sine = make_oscil(speed) scl = 0.5 * depth offset = 1.0 - scl lambda do |inval| inval * (offset + scl * oscil(sine)) end end # hello-dentist # # CLM instrument version is in clm.html add_help(:hello_dentist, "hello_dentist(frq, amp, snd=false, chn=false) \ Varies the sampling rate randomly, \ making a voice sound quavery: hello_dentist(40.0, 0.1)") def hello_dentist(freq, amp, snd = false, chn = false) rn = make_rand_interp(:frequency, freq, :amplitude, amp) i = 0 len = framples() in_data = channel2vct(0, len, snd, chn) out_len = (len * (1.0 + 2.0 * amp)).to_i out_data = make_vct(out_len) rd = make_src(:srate, 1.0, :input, lambda do |dir| val = i.between?(0, len - 1) ? in_data[i] : 0.0 i += dir val end) vct2channel(out_data.map do |x| src(rd, rand_interp(rn)) end, 0, len, snd, chn, false, format("%s(%s, %s", get_func_name, freq, amp)) end # a very similar function uses oscil instead of rand-interp, giving # various "Forbidden Planet" sound effects: add_help(:fp, "fp(sr, osamp, osfrq, snd=false, chn=false) \ Varies the sampling rate via an oscil: fp(1.0, 0.3, 20)") def fp(sr, osamp, osfreq, snd = false, chn = false) os = make_oscil(:frequency, osfreq) s = make_src(:srate, sr) len = framples(snd, chn) sf = make_sampler(0, snd, chn) out_data = make_vct!(len) do src(s, osamp * oscil(os), lambda do |dir| dir > 0 ? next_sample(sf) : previous_sample(sf) end) end free_sampler(sf) vct2channel(out_data, 0, len, snd, chn, false, format("%s(%s, %s, %s", get_func_name, sr, osamp, osfreq)) end # compand Compand_table = vct(-1.0, -0.96, -0.9, -0.82, -0.72, -0.6, -0.45, -0.25, 0.0, 0.25, 0.45, 0.6, 0.72, 0.82, 0.9, 0.96, 1.0) add_help(:compand, "compand() \ Returns a compander: map_channel(compand())") def compand() lambda do |inval| array_interp(Compand_table, 8.0 + 8.0 * inval, Compand_table.length) end end # shift pitch keeping duration constant # # both src and granulate take a function argument to get input # whenever it is needed. in this case, src calls granulate which # reads the currently selected file. CLM version is in expsrc.ins add_help(:expsrc, "expsrc(rate, snd=false, chn=false) \ Uses sampling-rate conversion and granular synthesis to produce a sound \ at a new pitch but at the original tempo. \ It returns a function for map_chan.") def expsrc(rate, snd = false, chn = false) gr = make_granulate(:expansion, rate) sr = make_src(:srate, rate) vsize = 1024 vbeg = 0 v = channel2vct(0, vsize) inctr = 0 lambda do |inval| src(sr, 0.0, lambda do |dir| granulate(gr, lambda do |dr| val = v[inctr] inctr += dr if inctr >= vsize vbeg += inctr inctr = 0 v = channel2vct(vbeg, vsize, snd, chn) end val end) end) end end # the next (expsnd) changes the tempo according to an envelope; the # new duration will depend on the expansion envelope -- we integrate # it to get the overall expansion, then use that to decide the new # length. add_help(:expsnd, "expsnd(gr_env, snd=false, chn=false) \ Uses the granulate generator to change tempo \ according to an envelope: expsnd([0, 0.5, 2, 2.0])") def expsnd(gr_env, snd = false, chn = false) dur = ((framples(snd, chn) / srate(snd)) * integrate_envelope(gr_env) / envelope_last_x(gr_env)) gr = make_granulate(:expansion, gr_env[1], :jitter, 0) ge = make_env(:envelope, gr_env, :duration, dur) sound_len = (srate(snd) * dur).to_i len = [sound_len, framples(snd, chn)].max sf = make_sampler(0, snd, chn) out_data = make_vct!(len) do val = granulate(gr, lambda do |dir| next_sample(sf) end) gr.increment = env(ge) val end free_sampler(sf) vct2channel(out_data, 0, len, snd, chn, false, format("%s(%s", get_func_name, gr_env.inspect)) end # cross-synthesis # # CLM version is in clm.html add_help(:cross_synthesis, "cross_synthesis(cross_snd, amp, fftsize, r) \ Does cross-synthesis between CROSS_SND (a sound index) \ and the currently selected sound: \ map_channel(cross_synthesis(1, 0.5, 128, 6.0))") def cross_synthesis(cross_snd, amp, fftsize, r) freq_inc = fftsize / 2 fdr = make_vct(fftsize) fdi = make_vct(fftsize) spectr = make_vct(freq_inc) inctr = 0 ctr = freq_inc radius = 1.0 - r / fftsize.to_f osr = mus_srate() csr = srate() bin = csr / fftsize # The comment from examp.scm applies of course here as well: # ;; if mus-srate is 44.1k and srate is 48k, make-formant # ;; thinks we're trying to go past srate/2 # ;; and in any case it's setting its formants incorrectly # ;; for the actual output srate # begin of temporary mus-srate set_mus_srate(csr) fmts = make_array(freq_inc) do |i| make_formant(i * bin, radius) end set_mus_srate(osr) # end of temporary mus-srate formants = make_formant_bank(fmts, spectr) lambda do |inval| if ctr == freq_inc fdr = channel2vct(inctr, fftsize, cross_snd, 0) inctr += freq_inc spectrum(fdr, fdi, false, 2) vct_subtract!(fdr, spectr) vct_scale!(fdr, 1.0 / freq_inc) ctr = 0 end ctr += 1 vct_add!(spectr, fdr) amp * formant_bank(formants, inval) end end # similar ideas can be used for spectral cross-fades, etc -- for example: add_help(:voiced2unvoiced, "voiced2unvoiced(amp, fftsize, r, tempo, snd=false, chn=false) \ Turns a vocal sound into whispering: voiced2unvoiced(1.0, 256, 2.0, 2.0)") def voiced2unvoiced(amp, fftsize, r, tempo, snd = false, chn = false) freq_inc = fftsize / 2 fdr = make_vct(fftsize) fdi = make_vct(fftsize) spectr = make_vct(freq_inc) noi = make_rand(:frequency, srate(snd) / 3.0) inctr = 0 ctr = freq_inc osr = mus_srate() csr = srate() # See cross_synthesis above. # begin of temporary mus-srate set_mus_srate(csr) radius = 1.0 - r.to_f / fftsize bin = csr / fftsize len = framples(snd, chn) outlen = (len / tempo).floor hop = (freq_inc * tempo).floor out_data = make_vct([len, outlen].max) fmts = make_array(freq_inc) do |i| make_formant(i * bin, radius) end set_mus_srate(osr) # end of temporary mus-srate formants = make_formant_bank(fmts, spectr) old_peak_amp = new_peak_amp = 0.0 outlen.times do |i| if ctr == freq_inc fdr = channel2vct(inctr, fftsize, snd, chn) if (pk = vct_peak(fdr)) > old_peak_amp old_peak_amp = pk end spectrum(fdr, fdi, false, 2) inctr += hop vct_subtract!(fdr, spectr) vct_scale!(fdr, 1.0 / freq_inc) ctr = 0 end ctr += 1 vct_add!(spectr, fdr) if (outval = formant_bank(formants, rand(noi))).abs > new_peak_amp new_peak_amp = outval.abs end out_data[i] = outval end vct_scale!(out_data, amp * (old_peak_amp / new_peak_amp)) vct2channel(out_data, 0, [len, outlen].max, snd, chn, false, format("%s(%s, %s, %s, %s", get_func_name, amp, fftsize, r, tempo)) end # very similar but use sum-of-cosines (glottal pulse train?) # instead of white noise add_help(:pulse_voice, "pulse_voice(cosin, freq=440.0, amp=1.0, fftsize=256, r=2.0, \ snd=false, chn=false) \ Use ncos to manipulate speech sounds.") def pulse_voice(cosin, freq = 440.0, amp = 1.0, fftsize = 256, r = 2.0, snd = false, chn = false) freq_inc = fftsize / 2 spectr = make_vct(freq_inc) len = framples(snd, chn) osr = mus_srate() csr = srate(snd) # See cross_synthesis above. # begin of temporary mus-srate set_mus_srate(csr) bin = csr / fftsize radius = 1.0 - (r.to_f / fftsize) fmts = make_array(freq_inc) do |i| make_formant(:radius, radius, :frequency, i * bin) end formants = make_formant_bank(fmts, spectr) set_mus_srate(osr) # end of temporary mus-srate old_peak_amp = 0.0 pulse = make_ncos(freq, cosin) fdr = nil inctr = 0 fdi = make_vct(fftsize) inv_freq_inc = 1.0 / freq_inc out_data = make_vct!(len) do |i| if i.modulo(freq_inc) == 0 fdr = channel2vct(inctr, fftsize, snd, chn) old_peak_amp = [vct_peak(fdr), old_peak_amp].max spectrum(fdr, fdi, false, 2) vct_subtract!(fdr, spectr) vct_scale!(fdr, inv_freq_inc) inctr += freq_inc end vct_add!(spectr, fdr) formant_bank(formants, ncos(pulse)) end vct_scale!(out_data, (old_peak_amp / vct_peak(out_data)) * amp) vct2channel(out_data, 0, len, snd, chn) end # pulse_voice(80, 20.0, 1.0, 1024, 0.01) # pulse_voice(80, 120.0, 1.0, 1024, 0.2) # pulse_voice(30, 240.0, 1.0, 1024, 0.1) # pulse_voice(30, 240.0, 1.0, 2048) # pulse_voice( 6, 1000.0, 1.0, 512) # convolution example add_help(:cnvtest, "cnvtest(snd0, snd1, amp) \ Convolves snd0 and snd1, scaling by amp, \ returns new max amp: cnvtest(0, 1, 0.1)") def cnvtest(snd0, snd1, amp) flt_len = framples(snd0) total_len = flt_len + framples(snd1) cnv = make_convolve(:filter, channel2vct(0, flt_len, snd0)) sf = make_sampler(0, snd1, false) out_data = make_vct!(total_len) do convolve(cnv, lambda do |dir| next_sample(sf) end) end free_sampler(sf) vct_scale!(out_data, amp) max_samp = vct_peak(out_data) vct2channel(out_data, 0, total_len, snd1) if max_samp > 1.0 set_y_bounds(snd1, [-max_samp, max_samp]) end max_samp end # swap selection chans add_help(:swap_selection_channels, "swap_selection_channels() \ Swaps the currently selected data's channels.") def swap_selection_channels if (not selection?) Snd.raise(:no_active_selection) end if selection_chans != 2 Snd.raise(:wrong_number_of_channels, "need a stereo selection") end beg = selection_position() len = selection_framples() snd_chn0 = snd_chn1 = nil Snd.sounds.each do |snd| channels(snd).times do |chn| if selection_member?(snd, chn) if snd_chn0.nil? snd_chn0 = [snd, chn] elsif snd_chn1.nil? snd_chn1 = [snd, chn] break end end end end if snd_chn1.nil? Snd.raise(:wrong_number_of_channels, "need two channels to swap") end swap_channels(snd_chn0[0], snd_chn0[1], snd_chn1[0], snd_chn1[1], beg, len) end # sound interp # # make-sound-interp sets up a sound reader that reads a channel at # an arbitary location, interpolating between samples if necessary, # the corresponding "generator" is sound-interp add_help(:make_sound_interp, "make_sound_interp(start, snd=false, chn=false) \ An interpolating reader for SND's channel CHN.") def make_sound_interp(start, snd = false, chn = false) data = channel2vct(0, false, snd, chn) size = data.length lambda do |loc| array_interp(data, loc, size) end end add_help(:sound_interp, "sound_interp(func, loc) \ Sample at LOC (interpolated if necessary) from FUNC \ created by make_sound_interp.") def sound_interp(func, loc) func.call(loc) end def sound_via_sound(snd1, snd2) intrp = make_sound_interp(0, snd1, 0) len = framples(snd1, 0) - 1 rd = make_sampler(0, snd2, 0) mx = maxamp(snd2, 0) map_channel(lambda do |val| sound_interp(intrp, (len * 0.5 * (1.0 + (read_sample(rd) / mx))).floor) end) end # env_sound_interp takes an envelope that goes between 0 and 1 # (y-axis), and a time-scaler (1.0 = original length) and returns a # new version of the data in the specified channel that follows that # envelope (that is, when the envelope is 0 we get sample 0, when # the envelope is 1 we get the last sample, envelope = 0.5 we get # the middle sample of the sound and so on. env_sound_interp([0, 0, # 1, 1]) will return a copy of the current sound; # env_sound_interp([0, 0, 1, 1, 2, 0], 2.0) will return a new sound # with the sound copied first in normal order, then reversed. # src_sound with an envelope could be used for this effect, but it # is much more direct to apply the envelope to sound sample # positions. add_help(:env_sound_interp, "env_sound_interp(env, time_scale=1.0, snd=false, chn=false) \ Reads SND's channel CHN according to ENV and TIME_SCALE.") def env_sound_interp(envelope, time_scale = 1.0, snd = false, chn = false) len = framples(snd, chn) newlen = (time_scale.to_f * len).floor read_env = make_env(:envelope, envelope, :length, newlen + 1, :scaler, len) data = channel2vct(0, false, snd, chn) new_snd = Vct.new(newlen) do array_interp(data, env(read_env), len) end set_samples(0, newlen, new_snd, snd, chn, true, format("%s(%p, %s", get_func_name, envelope, time_scale), 0, Current_edit_position, true) end # env_sound_interp([0, 0, 1, 1, 2, 0], 2.0) add_help(:granulated_sound_interp, "granulated_sound_interp(env, time_scale=1.0, grain_len=0.1, \ grain_env=[0, 0, 1, 1, 2, 1, 3, 0], out_hop=0.05, snd=false, chn=false) \ Reads the given channel following ENV (as in env_sound_interp), \ using grains to create the re-tempo'd read.") def granulated_sound_interp(envelope, time_scale = 1.0, grain_length = 0.1, grain_envelope = [0, 0, 1, 1, 2, 1, 3, 0], output_hop = 0.05, snd = false, chn = false) len = framples(snd, chn) newlen = (time_scale.to_f * len).floor read_env = make_env(envelope, :length, newlen, :scaler, len) sr = srate(snd).to_f grain_frames = (grain_length * sr).to_i hop_frames = (output_hop * sr).to_i num_readers = (grain_length.to_f / output_hop).ceil cur_readers = 0 next_reader = 0 jitter = sr * 0.005 readers = Array.new(num_readers, false) grain_envs = Array.new(num_readers) do make_env(grain_envelope, :length, grain_frames) end new_snd = Vct.new(newlen, 0.0) 0.step(newlen, hop_frames) do |i| stop = [newlen, hop_frames + i].min read_env.location = i position_in_original = env(read_env) mx = [0, (position_in_original + mus_random(jitter)).round].max readers[next_reader] = make_sampler(mx, snd, chn) grain_envs[next_reader].reset next_reader += 1 next_reader %= num_readers if cur_readers < next_reader cur_readers = next_reader end (0...cur_readers).each do |j| en = grain_envs[j] rd = readers[j] (i...stop).each do |k| new_snd[k] = env(en) * rd.call() end end end set_samples(0, newlen, new_snd, snd, chn, true, format("%s(%p, %s, %s, %p, %s", get_func_name, envelope, time_scale, grain_length, grain_envelope, output_hop), 0, Current_edit_position, true) end # granulated_sound_interp([0, 0, 1, 0.1, 2, 1], 1.0, 0.2, [0, 0, 1, 1, 2, 0]) # granulated_sound_interp([0, 0, 1, 1], 2.0) # granulated_sound_interp([0, 0, 1, 0.1, 2, 1], 1.0, 0.2, [0, 0, 1, 1, 2, 0], # 0.02) # filtered-env add_help(:filtered_env, "filtered_env(env, snd=false, chn=false) \ Is a time-varying one-pole filter: when ENV is at 1.0, no filtering, \ as ENV moves to 0.0, low-pass gets more intense; \ amplitude and low-pass amount move together.") def filtered_env(en, snd = false, chn = false) flt = make_one_pole(1.0, 0.0) amp_env = make_env(:envelope, en, :length, framples()) map_channel(lambda do |val| env_val = env(amp_env) set_mus_xcoeff(flt, 0, env_val) set_mus_ycoeff(flt, 1, env_val - 1.0) one_pole(flt, env_val * val) end, 0, false, snd, chn, false, format("%s(%s", get_func_name, en.inspect)) end # lisp graph with draggable x axis class Mouse def initialize @down = 0 @pos = 0.0 @x1 = 1.0 end def press(snd, chn, button, state, x, y) @pos = x / @x1 @down = @x1 end def drag(snd, chn, button, state, x, y) xnew = x / @x1 @x1 = [1.0, [0.1, @down + (@pos - xnew)].max].min graph(make_vct!((100 * @x1).floor) do |i| i * 0.01 end, "ramp", 0.0, @x1) end end =begin let(Mouse.new) do |mouse| $mouse_drag_hook.add_hook!("Mouse") do |snd, chn, button, state, x, y| mouse.drag(snd, chn, button, state, x, y) end $mouse_press_hook.add_hook!("Mouse") do |snd, chn, button, state, x, y| mouse.press(snd, chn, button, state, x, y) end end =end # pointer focus within Snd # # $mouse_enter_graph_hook.add_hook!("focus") do |snd, chn| # focus_widget(channel_widgets(snd, chn)[0]) # end # $mouse_enter_listener_hook.add_hook!("focus") do |widget| # focus_widget(widget) # end # $mouse_enter_text_hook.add_hook!("focus") do |widget| # focus_widget(widget) # end # View: Files dialog chooses which sound is displayed # # by Anders Vinjar add_help(:files_popup_buffer, "files_popup_buffer(type, position, name) \ Hides all sounds but the one the mouse touched in the current files list. \ Use with $mouse_enter_label_hook. $mouse_enter_label_hook.add_hook!(\"files-popup\") do |type, position, name| files_popup_buffer(type, position, name) end") def files_popup_buffer(type, position, name) if snd = find_sound(name) curr_buffer = Snd.snd vals = widget_size(sound_widgets(curr_buffer)[0]) height = vals[1] Snd.sounds.each do |s| hide_widget(sound_widgets(s)[0]) end show_widget(sound_widgets(snd)[0]) set_widget_size(sound_widgets(snd)[0], [widht, height]) select_sound(snd) end end # remove-clicks add_help(:find_click, "find_click(loc) \ Finds the next click starting at LOC.") def find_click(loc) reader = make_sampler(loc, false, false) samp0 = samp1 = samp2 = 0.0 samps = make_vct(10) len = framples() samps_ctr = 0 (loc...len).each do |ctr| samp0, samp1, samp2 = samp1, samp2, next_sample(reader) samps[samps_ctr] = samp0 if samps_ctr < 9 samps_ctr += 1 else samps_ctr = 0 end local_max = [0.1, vct_peak(samps)].max if ((samp0 - samp1).abs > local_max) and ((samp1 - samp2).abs > local_max) and ((samp0 - samp2).abs < (local_max / 2)) return ctr - 1 end end false end add_help(:remove_clicks, "remove_clicks() \ Tries to find and smooth-over clicks.") def remove_clicks loc = 0 while (click = find_click(loc)) smooth_sound(click - 2, 4) loc = click + 2 end end # searching examples (zero+, next-peak) add_help(:search_for_click, "search_for_click() \ Looks for the next click (for use with C-s).") def search_for_click samp0 = samp1 = samp2 = 0.0 samps = Vct.new(10) sctr = 0 lambda do |val| samp0, samp1, samp2 = samp1, samp2, val samps[sctr] = val sctr += 1 if sctr >= 10 then sctr = 0 end local_max = [0.1, samps.peak].max if ((samp0 - samp1).abs >= local_max) and ((samp1 - samp2).abs >= local_max) and ((samp0 - samp2).abs <= (local_max / 2)) -1 else false end end end add_help(:zero_plus, "zero_plus() \ Finds the next positive-going \ zero crossing (if searching forward) (for use with C-s).") def zero_plus lastn = 0.0 lambda do |n| rtn = lastn < 0.0 and n >= 0.0 and -1 lastn = n rtn end end add_help(:next_peak, "next_peak() \ Finds the next max or min point \ in the time-domain waveform (for use with C-s).") def next_peak last0 = last1 = false lambda do |n| rtn = number?(last0) and ((last0 < last1 and last1 > n) or (last0 > last1 and last1 < n)) and -1 last0, last1 = last1, n rtn end end add_help(:find_pitch, "find_pitch(pitch) \ Finds the point in the current sound where PITCH (in Hz) \ predominates -- C-s find_pitch(300). \ In most cases, \ this will be slightly offset from the true beginning of the note.") def find_pitch(pitch) interpolated_peak_offset = lambda do |la, ca, ra| pk = 0.001 + [la, ca, ra].max logla = log([la, 0.0000001].max / pk) / log(10) logca = log([ca, 0.0000001].max / pk) / log(10) logra = log([ra, 0.0000001].max / pk) / log(10) 0.5 * (logla - logra) / ((logla + logra) - 2 * logca) end data = make_vct(transform_size) data_loc = 0 lambda do |n| data[data_loc] = n data_loc += 1 rtn = false if data_loc == transform_size data_loc = 0 if vct_peak(data) > 0.001 spectr = snd_spectrum(data, Rectangular_window, transform_size) pk = 0.0 pkloc = 0 (transform_size / 2).times do |i| if spectr[i] > pk pk = spectr[i] pkloc = i end end pit = (pkloc + (pkloc > 0 ? interpolated_peak_offset.call(*spectr[pkloc - 1, 3]) : 0.0) * srate()) / transform_size if (pitch - pit).abs < srate / (2 * transform_size) rtn = -(transform_size / 2) end end vct_fill!(data, 0.0) end rtn end end # file2vct and a sort of cue-list, I think add_help(:file2vct, "file2vct(file) \ Returns a vct with FILE's data.") def file2vct(file) len = mus_sound_framples(file) reader = make_sampler(0, file) data = make_vct!(len) do next_sample(reader) end free_sampler(reader) data end add_help(:add_notes, "add_notes(notes, snd=false, chn=false) \ Adds (mixes) NOTES which is a list of lists of the form: \ [file, offset=0.0, amp=1.0] starting at the cursor in the \ currently selected channel: \ add_notes([[\"oboe.snd\"], [\"pistol.snd\", 1.0, 2.0]])") def add_notes(notes, snd = false, chn = false) start = cursor(snd, chn) as_one_edit_rb("%s(%s", get_func_name, notes.inspect) do (notes or []).each do |note| file, offset, amp = note beg = start + (srate(snd) * (offset or 0.0)).floor if amp and amp != 1.0 mix_vct(vct_scale!(file2vct(file), amp), beg, snd, chn, false, format("%s(%s", get_func_name, notes.inspect)) else mix(file, beg, 0, snd, chn, false) end end end end add_help(:region_play_list, "region_play_list(data) \ DATA is list of lists [[time, reg], ...], time in secs, \ setting up a sort of play list: \ region_play_list([[0.0, 0], [0.5, 1], [1.0, 2], [1.0, 0]])") def region_play_list(data) (data or []).each do |tm, rg| tm = (1000.0 * tm).floor if region?(rg) call_in(tm, lambda do | | play(rg) end) end end end add_help(:region_play_sequence, "region_play_sequence(data) \ DATA is list of region ids which will be played one after the other: \ region_play_sequence([0, 2, 1])") def region_play_sequence(data) time = 0.0 region_play_list(data.map do |id| cur = time time = time + region_framples(id) / region_srate(id) [cur, id] end) end # replace-with-selection add_help(:replace_with_selection, "replace_with_selection() \ Replaces the samples from the cursor with the current selection.") def replace_with_selection beg = cursor len = selection_framples() delete_samples(beg, len) insert_selection(beg) end # explode-sf2 add_help(:explode_sf2, "explode_sf2() \ Turns the currently selected soundfont file into \ a bunch of files of the form sample-name.aif.") def explode_sf2 (soundfont_info() or []).each do |name, start, loop_start, loop_end| filename = name + ".aif" if selection? set_selection_member?(false, true) end set_selection_member?(true) set_selection_position(start) set_selection_framples(framples() - start) save_selection(filename, selection_srate(), Mus_bshort, Mus_aifc) temp = open_sound(filename) set_sound_loop_info([loop_start, loop_end], temp) close_sound(temp) end end # open-next-file-in-directory class Next_file def initialize @last_file_opened = "" @current_directory = "" @current_sorted_files = [] end def open_next_file_in_directory unless $open_hook.member?("open-next-file-in-directory") $open_hook.add_hook!("open-next-file-in-directory") do |fname| self.get_current_directory(fname) end end if @last_file_opened.empty? and sounds @last_file_opened = file_name(Snd.snd) end if @current_directory.empty? unless sounds get_current_files(Dir.pwd) else get_current_files(File.split(@last_file_opened).first) end end if @current_sorted_files.empty? Snd.raise(:no_such_file) else next_file = find_next_file if find_sound(next_file) Snd.raise(:file_already_open, next_file) else sounds and close_sound(Snd.snd) open_sound(next_file) end end true end private def find_next_file choose_next = @last_file_opened.empty? just_filename = File.basename(@last_file_opened) f = callcc do |ret| @current_sorted_files.each do |file| ret.call(file) if choose_next if file == just_filename choose_next = true end end @current_sorted_files[0] # wrapped around end @current_directory + "/" + f end def get_current_files(dir) @current_directory = dir @current_sorted_files = sound_files_in_directory(dir).sort end def get_current_directory(filename) Snd.display(@last_file_opened = filename) new_path = File.split(mus_expand_filename(filename)).first if @current_directory.empty? or @current_directory != new_path get_current_files(new_path) end false end end def click_middle_button_to_open_next_file_in_directory nf = Next_file.new $mouse_click_hook.add_hook!("next-file") do |s, c, button, st, x, y, ax| if button == 2 nf.open_next_file_in_directory end end end # chain-dsps def chain_dsps(start, dur, *dsps) dsp_chain = dsps.map do |gen| if array?(gen) make_env(:envelope, gen, :duration, dur) else gen end end run_instrument(start, dur) do val = 0.0 dsp_chain.each do |gen| if env?(gen) val *= gen.run elsif readin?(gen) val += gen.run else val = gen.run(val) end end val end end =begin with_sound() do chain_dsps(0, 1.0, [0, 0, 1, 1, 2, 0], make_oscil(:frequency, 440)) chain_dsps(0, 1.0, [0, 0, 1, 1, 2, 0], make_one_pole(0.5), make_readin("oboe.snd")) chain_dsps(0, 1.0, [0, 0, 1, 1, 2, 0], let(make_oscil(:frequency, 220), make_oscil(:frequency, 440)) do |osc1, osc2| lambda do |val| osc1.run(val) + osc2.run(2.0 * val) end end) end =end # re-order channels def scramble_channels(*new_order) len = new_order.length swap_once = lambda do |current, desired, n| if n != len cur_orig, cur_cur = current[n][0, 2] dst = desired[n] if cur_orig != dst swap_channels(false, cur_cur, false, dst) current[dst][0] = cur_orig end swape_once.call(current, desired, n + 1) end end swap_once.call(make_array(len) do |i| [i, i] end, new_order, 0) end def scramble_channel(silence) buffer = make_moving_average(128) silence = silence / 128.0 edges = [] in_silence = true old_max = max_regions old_tags = with_mix_tags set_max_regions(1024) set_with_mix_tags(false) rd = make_sampler() framples().times do |i| y = next_sample(rd) sum_of_squares = moving_average(buffer, y * y) now_silent = (sum_of_squares < silence) if now_silent != in_silence edges.push(i) end in_silence = now_silent end edges.push(framples()) len = edges.length start = 0 pieces = Array.new(len) do |i| en = edges[i] re = make_region(start, en) start = en re end start = 0 fnc = lambda do | | scale_by(0.0) len.times do |i| this = random(len) reg = pieces[this] pieces[this] = false unless reg (this + 1).upto(len - 1) do |j| reg = pieces[j] if reg pieces[j] = false break end end unless reg (this - 1).downto(0) do |j| reg = pieces[j] if reg pieces[j] = false break end end end end mix_region(reg, start) start += framples(reg) forget_region(reg) end end as_one_edit(fnc) set_max_regions(old_max) set_with_mix_tags(old_tags) end # scramble_channel(0.01) # reorder blocks within channel add_help(:reverse_by_blocks, "reverse_by_blocks(block_len, snd=false, chn=false) \ Divide sound into block-len blocks, recombine blocks in reverse order.") def reverse_by_blocks(block_len, snd = false, chn = false) len = framples(snd, chn) num_blocks = (len / (srate(snd).to_f * block_len)).floor if num_blocks > 1 actual_block_len = len / num_blocks rd = make_sampler(len - actual_block_len, snd, chn) beg = 0 ctr = 1 map_channel(lambda do |y| val = read_sample(rd) if beg < 10 val = val * beg * 0.1 else if beg > actual_block_len - 10 val = val * (actual_block_len - beg) * 0.1 end end beg += 1 if beg == actual_block_len ctr += 1 beg = 0 rd = make_sampler([len - ctr * actual_block_len, 0].max, snd, chn) end val end, 0, false, snd, chn, false, format("%s(%s", get_func_name, block_len)) end end add_help(:reverse_within_blocks, "reverse_within_blocks(block_len, snd=false, chn=false) \ Divide sound into blocks, recombine in order, \ but each block internally reversed.") def reverse_within_blocks(block_len, snd = false, chn = false) len = framples(snd, chn) num_blocks = (len / (srate(snd).to_f * block_len)).floor if num_blocks > 1 actual_block_len = len / num_blocks no_clicks_env = [0.0, 0.0, 0.01, 1.0, 0.99, 1.0, 1.0, 0.0] as_one_edit(lambda do | | 0.step(len, actual_block_len) do |beg| reverse_channel(beg, actual_block_len, snd, chn) env_channel(no_clicks_env, beg, actual_block_len, snd, chn) end end, format("%s(%s", get_func_name, block_len)) else reverse_channel(0, false, snd, chn) end end def segment_maxamp(name, beg, dur) mx = 0.0 rd = make_sampler(beg, name) dur.times do mx = [mx, next_sample(rd).abs].max end free_sampler(rd) mx end def segment_sound(name, high, low) len = mus_sound_framples(name) reader = make_sampler(0, name) avg = make_moving_average(:size, 128) lavg = make_moving_average(:size, 2048) segments = Vct.new(100) segctr = 0 possible_end = 0 in_sound = false len.times do |i| samp = next_sample(reader).abs val = moving_average(avg, samp) lval = moving_average(lavg, samp) if in_sound if val < low possible_end = i if lval < low segments[segctr] = possible_end + 128 segctr += 1 in_sound = false end else if val > high segments[segctr] = i - 128 segctr += 1 in_sound = true end end end end free_sampler(reader) if in_sound segments[segctr] = len [segctr + 1, segments] else [segctr, segments] end end def do_one_directory(fd, dir_name, ins_name, high = 0.01, low = 0.001) snd_print("# #{dir_name}") sound_files_in_directory(dir_name).each do |sound| sound_name = dir_name + "/" + sound boundary_data = segment_sound(sound_name, high, low) segments, boundaries = boundary_data fd.printf("\n\n# ", sound) fd.printf("(%s %s", ins_name, sound_name.inspect) 0.step(segments, 2) do |bnd| segbeg = boundaries[bnd].to_i segbeg = boundaries[bnd + 1].to_i fd.printf("(%s %s %s)", segbeg, segdur, segment_maxamp(sound_name, segbeg, segdur)) fd.printf(")") end mus_sound_forget(sound_name) end end def sound2segment_data(main_dir, output_file = "sounds.data") File.open(output_file, "w") do |fd| old_fam = with_file_monitor set_with_file_monitor(false) fd.printf("# sound data from %s", main_dir.inspect) if main_dir[-1] != "/" then main_dir += "/" end Dir[main_dir].each do |dir| ins_name = dir.downcase.tr(" ", "") fd.printf("\n\n# ---------------- %s ----------------", dir) if dir == "Piano" Dir[main_dir + dir].each do |inner_dir| do_one_directory(fd, main_dir + dir + "/" + inner_dir, ins_name, 0.001, 0.0001) end else do_one_directory(fd, main_dir + dir, ins_name) end end set_with_file_monitor(old_fam) end end # sounds2segment_data(ENV['HOME'] + "/.snd.d/iowa/sounds/", "iowa.data") add_help(:channel_clipped?, "channel_clipped?(snd=false, chn=false) \ Returns true and a sample number if it finds clipping.") def channel_clipped?(snd = false, chn = false) last_y = 0.0 scan_channel(lambda do |y| result = (y.abs >= 0.9999 and last_y.abs >= 0.9999) last_y = y result end, 0, false, snd, chn) end # scan-sound def scan_sound(func, beg = 0, dur = false, snd = false) if sound?(index = Snd.snd(snd)) if (chns = channels(index)) == 1 scan_channel(lambda do |y| func.call(y, 0) end, beg, dur, index, 0) else len = framples(index) fin = (dur ? [len, beg + dur].min : len) readers = make_array(chns) do |chn| make_sampler(beg, index, chn) end result = false beg.upto(fin) do |i| local_result = true readers.each_with_index do |rd, chn| local_result = (func.call(rd.call, chn) and local_result) end if local_result result = [true, i] break end end result end else Snd.raise(:no_such_sound, get_func_name, snd) end end def scan_sound_rb(beg = 0, dur = false, snd = false, &func) scan_sound(func, beg, dur, snd) end end include Examp module Moog class Moog_filter < Musgen Gaintable = vct(0.999969, 0.990082, 0.980347, 0.970764, 0.961304, 0.951996, 0.94281, 0.933777, 0.924866, 0.916077, 0.90741, 0.898865, 0.890442, 0.882141, 0.873962, 0.865906, 0.857941, 0.850067, 0.842346, 0.834686, 0.827148, 0.819733, 0.812378, 0.805145, 0.798004, 0.790955, 0.783997, 0.77713, 0.770355, 0.763672, 0.75708, 0.75058, 0.744141, 0.737793, 0.731537, 0.725342, 0.719238, 0.713196, 0.707245, 0.701355, 0.695557, 0.689819, 0.684174, 0.678558, 0.673035, 0.667572, 0.66217, 0.65686, 0.651581, 0.646393, 0.641235, 0.636169, 0.631134, 0.62619, 0.621277, 0.616425, 0.611633, 0.606903, 0.602234, 0.597626, 0.593048, 0.588531, 0.584045, 0.579651, 0.575287, 0.570953, 0.566681, 0.562469, 0.558289, 0.554169, 0.550079, 0.546051, 0.542053, 0.538116, 0.53421, 0.530334, 0.52652, 0.522736, 0.518982, 0.515289, 0.511627, 0.507996, 0.504425, 0.500885, 0.497375, 0.493896, 0.490448, 0.487061, 0.483704, 0.480377, 0.477081, 0.473816, 0.470581, 0.467377, 0.464203, 0.46109, 0.457977, 0.454926, 0.451874, 0.448883, 0.445892, 0.442932, 0.440033, 0.437134, 0.434265, 0.431427, 0.428619, 0.425842, 0.423096, 0.42038, 0.417664, 0.415009, 0.412354, 0.409729, 0.407135, 0.404572, 0.402008, 0.399506, 0.397003, 0.394501, 0.392059, 0.389618, 0.387207, 0.384827, 0.382477, 0.380127, 0.377808, 0.375488, 0.37323, 0.370972, 0.368713, 0.366516, 0.364319, 0.362122, 0.359985, 0.357849, 0.355713, 0.353607, 0.351532, 0.349457, 0.347412, 0.345398, 0.343384, 0.34137, 0.339417, 0.337463, 0.33551, 0.333588, 0.331665, 0.329773, 0.327911, 0.32605, 0.324188, 0.322357, 0.320557, 0.318756, 0.316986, 0.315216, 0.313446, 0.311707, 0.309998, 0.308289, 0.30658, 0.304901, 0.303223, 0.301575, 0.299927, 0.298309, 0.296692, 0.295074, 0.293488, 0.291931, 0.290375, 0.288818, 0.287262, 0.285736, 0.284241, 0.282715, 0.28125, 0.279755, 0.27829, 0.276825, 0.275391, 0.273956, 0.272552, 0.271118, 0.269745, 0.268341, 0.266968, 0.265594, 0.264252, 0.262909, 0.261566, 0.260223, 0.258911, 0.257599, 0.256317, 0.255035, 0.25375) Freqtable = [0, -1, 0.03311111, -0.9, 0.06457143, -0.8, 0.0960272, -0.7, 0.127483, -0.6, 0.1605941, -0.5, 0.1920544, -0.4, 0.22682086, -0.3, 0.2615873, -0.2, 0.29801363, -0.1, 0.33278003, -0.0, 0.37086168, 0.1, 0.40893877, 0.2, 0.4536417, 0.3, 0.5, 0.4, 0.5463583, 0.5, 0.5943719, 0.6, 0.6556281, 0.7, 0.72185487, 0.8, 0.8096009, 0.9, 0.87913835, 0.95, 0.9933787, 1, 1, 1] def initialize(freq, q) super() @frequency = freq @Q = q @state = make_vct(4) @A = 0.0 @freqtable = envelope_interp(freq / (srate() * 0.5), Freqtable) end attr_reader :frequency, :state, :freqtable, :A attr_accessor :Q def inspect format("%s.new(%s, %s)", self.class, @frequency, @Q) end def to_s format("#<%s freq: %1.3f, Q: %s>", self.class, @frequency, @Q) end def run_func(val1 = 0.0, val2 = 0.0) filter(val1) end def frequency=(freq) @freqtable = envelope_interp(freq / (srate() * 0.5), Freqtable) @frequency = freq end def filter(insig) a = 0.25 * (insig - @A) @state.map! do |st| new_a = saturate(a + @freqtable * (a - st)) a = saturate(new_a + st) new_a end ix = @freqtable * 99.0 ixint = ix.floor ixfrac = ix - ixint @A = a * @Q * ((1.0 - ixfrac) * Gaintable[ixint + 99] + ixfrac * Gaintable[ixint + 100]) a end private def saturate(x) [[x, -0.95].max, 0.95].min end end add_help(:make_moog_filter, "make_moog_filter(freq=440.0, Q=0) \ Makes a new moog_filter generator. \ FREQUENCY is the cutoff in Hz, \ Q sets the resonance: 0 = no resonance, 1: oscillates at FREQUENCY.") def make_moog_filter(freq = 440.0, q = 0) Moog_filter.new(freq, q) end add_help(:moog_filter, "moog_filter(moog, insig=0.0) \ Is the generator associated with make_moog_filter.") def moog_filter(mg, insig = 0.0) mg.filter(insig) end def moog(freq, q) mg = Moog_filter.new(freq, q) lambda do |inval| mg.filter(inval) end end end include Moog # examp.rb ends here