1# examp.rb -- something from examp.scm
2
3# Translator/Author: Michael Scholz <mi-scholz@users.sourceforge.net>
4# Created: 02/09/04 18:34:00
5# Changed: 17/12/24 22:01:05
6
7# module Examp (examp.scm)
8#  selection_rms
9#  region_rms(n)
10#  window_samples(snd, chn)
11#  display_energy(snd, chn)
12#  display_db(snd, chn)
13#  window_rms
14#  fft_peak(snd, chn, scale)
15#  finfo(file)
16#  display_correlate(snd, chn, y0, y1)
17#
18#  zoom_spectrum(snd, chn, y0, y1)
19#  zoom_fft(snd, chn, y0, y1)
20#  superimpose_ffts(snd, chn, y0, y1)
21#  locate_zero(limit)
22#  shell(cmd, *rest)
23#
24#  mpg(mpgfile, rawfile)
25#  read_ogg(filename)
26#  write_ogg(snd)
27#  read_speex(filename)
28#  write_speex(snd)
29#  read_flac(filename)
30#  write_flac(snd)
31#  read_ascii(in_filename, out_filename, out_type, out_format, out_srate)
32#  auto_dot(snd, chn, y0, y1)
33#
34#  first_mark_in_window_at_left
35#  flash_selected_data(interval)
36#  mark_loops
37#  do_all_chans(origin) do |y| ... end
38#  update_graphs
39#  do_chans(*origin) do |y| ... end
40#  do_sound_chans(*origin) do |y| ... end
41#  every_sample? do |y| ... end
42#  sort_samples(nbins)
43#  place_sound(mono_snd, stereo_snd, pan_env)
44#
45#  fft_edit(bottom, top, snd, chn)
46#  fft_squelch(squelch, snd, chn)
47#  fft_cancel(lo_freq, hi_freq, snd, chn)
48#
49#  class Ramp < Musgen
50#   initialize(size)
51#   run_func(up, dummy)
52#   run(up)
53#
54#  make_ramp(size)
55#  ramp(gen, up)
56#  squelch_vowels(snd, chn)
57#  fft_env_data(fft_env, snd, chn)
58#  fft_env_edit(fft_env, snd, chn)
59#  fft_env_interp(env1, env2, interp, snd, chn)
60#  fft_smoother(cutoff, start, samps, snd, chn)
61#
62#  comb_filter(scaler, size)
63#  comb_chord(scaler, size, amp, interval_one, interval_two)
64#  zcomb(scaler, size, pm)
65#  notch_filter(scaler, size)
66#  formant_filter(radius, freq)
67#  formants(r1, f1, r2, f2, r3, f3)
68#  moving_formant(radius, move)
69#  osc_formants(radius, bases, amounts, freqs)
70#  echo(scaler, secs)
71#  zecho(scaler, secs, freq, amp)
72#  flecho(scaler, secs)
73#  ring_mod(freq, gliss_env)
74#  am(freq)
75#  vibro(speed, depth)
76#  hello_dentist(freq, amp, snd, chn)
77#  fp(sr, osamp, osfreq, snd, chn)
78#  compand()
79#  expsrc(rate, snd, chn)
80#  expsnd(gr_env, snd, chn)
81#  cross_synthesis(cross_snd, amp, fftsize, r)
82#  voiced2unvoiced(amp, fftsize, r, temp, snd, chn)
83#  pulse_voice(cosin, freq, amp, fftsize, r, snd, chn)
84#  cnvtest(snd0, snd1, amp)
85#
86#  swap_selection_channels
87#  make_sound_interp(start, snd, chn)
88#  sound_interp(func, loc)
89#  sound_via_sound(snd1, snd2)
90#  env_sound_interp(envelope, time_scale, snd, chn)
91#  granulated_sound_interp(en, time_scale, grain_len, grain_env, out_hop, s, c)
92#  filtered_env(en, snd, chn)
93#
94#  class Mouse
95#   initialize
96#   press(snd, chn, button, state, x, y)
97#   drag(snd, chn, button, state, x, y)
98#
99#  files_popup_buffer(type, position, name)
100#
101#  find_click(loc)
102#  remove_clicks
103#  search_for_click
104#  zero_plus
105#  next_peak
106#  find_pitch(pitch)
107#  file2vct(file)
108#  add_notes(notes, snd, chn)
109#  region_play_list(data)
110#  region_play_sequence(data)
111#  replace_with_selection
112#  explode_sf2
113#
114#  class Next_file
115#   initialize
116#   open_next_file_in_directory
117#  click_middle_button_to_open_next_file_in_directory
118#
119#  chain_dsps(start, dur, *dsps)
120#
121#  scramble_channels(*new_order)
122#  scramble_channel(silence)
123#
124#  reverse_by_blocks(block_len, snd, chn)
125#  reverse_within_blocks(block_len, snd, chn)
126#  sound2segment_data(main_dir, output_file)
127#  channel_clipped?(snd, chn)
128#  scan_sound(func, beg, dur, snd)
129#  or scan_sound_rb(beg, dur, snd) do |y, chn| ... end
130#
131# class Moog_filter < Musgen (moog.scm)
132#   initialize(freq, q)
133#   frequency=(freq)
134#   filter(insig)
135#
136# module Moog
137#  make_moog_filter(freq, q)
138#  moog_filter(moog, insig)
139#  moog(freq, q)
140
141require "clm"
142
143module Examp
144  # (ext)snd.html examples made harder to break
145  #
146  # this mainly involves keeping track of the current sound/channel
147
148  add_help(:selection_rms,
149           "selection_rms()  \
150Returns rms of selection data using samplers.")
151  def selection_rms
152    if selection?
153      reader = make_sampler(selection_position, false, false)
154      len = selection_framples()
155      sum = 0.0
156      len.times do
157        val = next_sample(reader)
158        sum = sum + val * val
159      end
160      free_sampler(reader)
161      sqrt(sum / len)
162    else
163      Snd.raise(:no_active_selection)
164    end
165  end
166
167  add_help(:region_rms,
168           "region_rms(n=0)  \
169Returns rms of region N's data (chan 0).")
170  def region_rms(n = 0)
171    if region?(n)
172      data = region2vct(n, 0, 0)
173      sqrt(dot_product(data, data) / data.length)
174    else
175      Snd.raise(:no_such_region)
176    end
177  end
178
179  add_help(:window_samples,
180           "window_samples(snd=false, chn=false)  \
181Returns samples in snd channel chn in current graph window.")
182  def window_samples(snd = false, chn = false)
183    wl = left_sample(snd, chn)
184    wr = right_sample(snd, chn)
185    channel2vct(wl, 1 + (wr - wl), snd, chn)
186  end
187
188  add_help(:display_energy,
189           "display_energy(snd, chn)  \
190Is a $lisp_graph_hook function to display the time domain \
191data as energy (squared):
192$lisp_graph_hook.add_hook!(\"display-energy\", \
193&method(:display_energy).to_proc)")
194  def display_energy(snd, chn)
195    ls = left_sample(snd, chn)
196    rs = right_sample(snd, chn)
197    datal = make_graph_data(snd, chn)
198    data = vct?(datal) ? datal : datal[1]
199    sr = srate(snd)
200    y_max = y_zoom_slider(snd, chn)
201    if data and ls and rs
202      vct_multiply!(data, data)
203      graph(data, "energy", ls / sr, rs / sr, 0.0, y_max * y_max, snd, chn)
204    end
205  end
206
207  add_help(:display_db,
208           "display_db(snd, chn)  \
209Is a $lisp_graph_hook function to display the time domain data in dB:
210$lisp_graph_hook.add_hook!(\"display-db\", &method(:display_db).to_proc)")
211  def display_db(snd, chn)
212    if datal = make_graph_data(snd, chn)
213      dB = lambda do |val|
214        if val < 0.001
215          -60.0
216        else
217          20.0 * log10(val)
218        end
219      end
220      data = vct?(datal) ? datal : datal[1]
221      sr = srate(snd)
222      ls = left_sample(snd, chn)
223      rs = right_sample(snd, chn)
224      data.map! do |val| 60.0 + dB.call(val.abs) end
225      graph(data, "dB", ls / sr, rs / sr, 0.0, 60.0, snd, chn)
226    end
227  end
228
229  add_help(:window_rms,
230           "window_rms()  \
231Returns rms of data in currently selected graph window.")
232  def window_rms
233    ls = left_sample
234    rs = right_sample
235    data = channel2vct(ls, 1 + (rs - ls))
236    sqrt(dot_product(data, data), data.length)
237  end
238
239  add_help(:fft_peak,
240           "fft_peak(snd, chn, scale)  \
241Returns the peak spectral magnitude:
242$after_transform_hook.add_hook!(\"fft-peak\") do |snd, chn, scale|
243  fft_peak(snd, chn, scale)
244end")
245  def fft_peak(snd, chn, scale)
246    if transform_graph? and transform_graph_type == Graph_once
247      pk = (2.0 * vct_peak(transform2vct(snd, chn))) / transform_size
248      status_report(pk.to_s, snd)
249      pk
250    else
251      false
252    end
253  end
254
255  # 'info' from extsnd.html using format
256
257  add_help(:finfo,
258           "finfo(file)  \
259Returns description (as a string) of FILE.")
260  def finfo(file)
261    chans = mus_sound_chans(file)
262    sr = mus_sound_srate(file)
263    format("%s: chans: %d, srate: %d, %s, %s, len: %1.3f",
264           file, chans, sr,
265           mus_header_type_name(mus_sound_header_type(file)),
266           mus_sample_type_name(mus_sound_sample_type(file)),
267           mus_sound_samples(file).to_f / (chans * sr.to_f))
268  end
269
270  # Correlation
271  #
272  # correlation of channels in a stereo sound
273
274  add_help(:display_correlate,
275           "display_correlate(snd, chn, y0, y1)  \
276Returns the correlation of SND's 2 channels (intended for use with $graph_hook):
277$graph_hook.add_hook!(\"display_correlate\") do |snd, chn, y0, y1|
278  display_correlate(snd, chn, y0, y1)
279end")
280
281  def display_correlate(snd, chn, y0, y1)
282    if channels(snd) == 2 and framples(snd, 0) > 1 and framples(snd, 1) > 1
283      ls = left_sample(snd, 0)
284      rs = right_sample(snd, 0)
285      ilen = 1 + (rs - ls)
286      pow2 = (log(ilen) / log(2)).ceil
287      fftlen = (2 ** pow2).to_i
288      fftlen2 = fftlen / 2
289      fftscale = 1.0 / fftlen
290      rl1 = channel2vct(ls, fftlen, snd, 0)
291      rl2 = channel2vct(ls, fftlen, snd, 1)
292      im1 = make_vct(fftlen)
293      im2 = make_vct(fftlen)
294      fft(rl1, im1, 1)
295      fft(rl2, im2, 1)
296      tmprl = vct_copy(rl1)
297      tmpim = vct_copy(im1)
298      data3 = make_vct(fftlen2)
299      vct_multiply!(tmprl, rl2)
300      vct_multiply!(tmpim, im2)
301      vct_multiply!(im2, rl1)
302      vct_multiply!(rl2, im1)
303      vct_add!(tmprl, tmpim)
304      vct_subtract!(im2, rl2)
305      fft(tmprl, im2, -1)
306      vct_add!(data3, tmprl)
307      vct_scale!(data3, fftscale)
308      graph(data3, "lag time", 0, fftlen2)
309    else
310      snd_print("correlate wants stereo input")
311    end
312  end
313
314  # set transform-size based on current time domain window size
315  #
316  # also zoom spectrum based on y-axis zoom slider
317
318  add_help(:zoom_spectrum,
319           "zoom_spectrum(snd, chn, y0, y1)  \
320Sets the transform size to correspond to the \
321time-domain window size (use with $graph_hook):
322$graph_hook.add_hook!(\"zoom-spectrum\") do |snd, chn, y0, y1|
323  zoom_spectrum(snd, chn, y0, y1)
324end")
325  def zoom_spectrum(snd, chn, y0, y1)
326    if transform_graph?(snd, chn) and
327       transform_graph_type(snd, chn) == Graph_once
328      set_transform_size((2 **
329                         (log(right_sample(snd, chn) -
330                              left_sample(snd, chn))/log(2.0))).to_i, snd, chn)
331      set_spectrum_end(y_zoom_slider(snd, chn), snd, chn)
332    end
333    false
334  end
335
336  add_help(:zoom_fft,
337           "zoom_fft(snd, chn, y0, y1)  \
338Sets the transform size if the time domain is \
339not displayed (use with $graph_hook).  \
340It also sets the spectrum display start point \
341based on the x position slider---this can be confusing \
342if fft normalization is on (the default):
343$graph_hook.add_hook!(\"zoom-fft\") do |snd, chn, y0, y1|
344  zoom_fft(snd, chn, y0, y1)
345end")
346  def zoom_fft(snd, chn, y0, y1)
347    if transform_graph?(snd, chn) and
348        (not time_graph?(snd, chn)) and
349        transform_graph_type(snd, chn) == Graph_once
350      set_transform_size(2 **
351                         (log(right_sample(snd, chn) -
352                              left_sample(snd, chn)) / log(2.0)).ceil, snd, chn)
353      set_spectrum_start(x_position_slider(snd, chn), snd, chn)
354      set_spectrum_end(y_zoom_slider(snd, chn), snd, chn)
355    end
356    false
357  end
358
359  # superimpose spectra of sycn'd sounds
360
361  add_help(:superimpose_ffts,
362           "superimpose_ffts(snd, chn, y0, y1)  \
363Superimposes ffts of multiple (syncd) sounds (use with $graph_hook):
364$graph_hook.add_hook!(\"superimpose-ffts\") do |snd, chn, y0, y1|
365  superimpose_ffts(snd, chn, y0, y1)
366end")
367  def superimpose_ffts(snd, chn, y0, y1)
368    maxsync = Snd.sounds.map do |s| sync(s) end.max
369    if sync(snd) > 0 and
370        snd == Snd.sounds.map do |s|
371        sync(snd) == sync(s) ? sound2integer(s) : (maxsync + 1)
372      end.min
373      ls = left_sample(snd, chn)
374      rs = right_sample(snd, chn)
375      pow2 = (log(rs - ls) / log(2)).ceil
376      fftlen = (2 ** pow2).to_i
377      if pow2 > 2
378        ffts = []
379        Snd.sounds.each do |s|
380          if sync(snd) == sync(s) and channels(s) > chn
381            fdr = channel2vct(ls, fftlen, s, chn)
382            fdi = make_vct(fftlen)
383            spectr = make_vct(fftlen / 2)
384            ffts.push(spectr.add(spectrum(fdr, fdi, false, 2)))
385          end
386        end
387        graph(ffts, "spectra", 0.0, 0.5, y0, y1, snd, chn)
388      end
389    end
390    false
391  end
392
393  # c-g? example (Anders Vinjar)
394
395  add_help(:locate_zero,
396           "locate_zero(limit)  \
397Looks for successive samples that sum to less than LIMIT, \
398moving the cursor if successful.")
399  def locate_zero(limit)
400    start = cursor
401    sf = make_sampler(start, false, false)
402    val0 = sf.call.abs
403    val1 = sf.call.abs
404    n = start
405    until sampler_at_end?(sf) or val0 + val1 < limit
406      val0, val1 = val1, sf.call.abs
407      n += 1
408    end
409    free_sampler(sf)
410    set_cursor(n)
411  end
412
413  # make a system call from irb or snd listener
414  #
415  #   shell("df") for example
416  # or to play a sound whenever a file is closed:
417  #   $close-hook.add_hook!() do |snd| shell("sndplay wood16.wav"); false end
418
419  add_help(:shell,
420           "shell(cmd, *rest)  \
421Sends CMD to a shell (executes it as a shell command) \
422and returns the result string.")
423  def shell(cmd, *rest)
424    str = ""
425    unless cmd.null?
426      IO.popen(format(cmd, *rest), "r") do |f| str = f.readlines.join end
427    end
428    str
429  end
430
431  # translate mpeg input to 16-bit linear and read into Snd
432  #
433  # mpg123 with the -s switch sends the 16-bit (mono or stereo) representation
434  #   of an mpeg file to stdout.  There's also apparently a switch to write
435  #   'wave' output.
436
437  add_help(:mpg,
438           "mpg(file, tmpname)  \
439Converts file from MPEG to raw 16-bit samples using mpg123: \
440mpg(\"mpeg.mpg\", \"mpeg.raw\")")
441  def mpg(mpgfile, rawfile)
442    b0 = b1 = b2 = b3 = 0
443    File.open(mpgfile, "r") do |fd|
444      b0 = fd.readchar
445      b1 = fd.readchar
446      b2 = fd.readchar
447      b3 = fd.readchar
448    end
449    if b0 != 255 or (b1 & 0b11100000) != 0b11100000
450      Snd.display("%s is not an MPEG file (first 11 bytes: %b %b",
451                  mpgfile.inspect, b0, b1 & 0b11100000)
452    else
453      id = (b1 & 0b11000) >> 3
454      layer = (b1 & 0b110) >> 1
455      srate_index = (b2 & 0b1100) >> 2
456      channel_mode = (b3 & 0b11000000) >> 6
457      if id == 1
458        Snd.display("odd: %s is using a reserved Version ID", mpgfile)
459      end
460      if layer == 0
461        Snd.display("odd: %s is using a reserved layer description", mpgfile)
462      end
463      chans = channel_mode == 3 ? 1 : 2
464      mpegnum = id.zero? ? 4 : (id == 2 ? 2 : 1)
465      mpeg_layer = layer == 3 ? 1 : (layer == 2 ? 2 : 3)
466      srate = [44100, 48000, 32000, 0][srate_index] / mpegnum
467      Snd.display("%s: %s Hz, %s, MPEG-%s",
468                  mpgfile, srate, chans == 1 ? "mono": "stereo", mpeg_layer)
469      system(format("mpg123 -s %s > %s", mpgfile, rawfile))
470      open_raw_sound(rawfile, chans, srate,
471                     little_endian? ? Mus_lshort : Mus_bshort)
472    end
473  end
474
475  # read and write OGG files
476
477  add_help(:read_ogg,
478           "read_ogg(filename)  \
479Read OGG files:
480$open_hook.add_hook!(\"read-ogg\") do |filename|
481  if mus_sound_header_type(filename) == Mus_raw
482    read_ogg(filename)
483  else
484    false
485  end
486end")
487  def read_ogg(filename)
488    flag = false
489    File.open(filename, "r") do |fd|
490      flag = fd.readchar == ?O and
491      fd.readchar == ?g and
492      fd.readchar == ?g and
493      fd.readchar == ?S
494    end
495    if flag
496      aufile = filename + ".au"
497      File.unlink(aufile) if File.exist?(aufile)
498      system(format("ogg123 -d au -f %s %s", aufile, filename))
499      aufile
500    else
501      false
502    end
503  end
504
505  def write_ogg(snd)
506    if edits(snd)[0] > 0 or header_type(snd) != Mus_riff
507      file = file_name(snd) + ".tmp"
508      save_sound_as(file, snd, :header_type, Mus_riff)
509      system("oggenc " + file)
510      File.unlink(file)
511    else
512      system("oggenc " + file_name(snd))
513    end
514  end
515
516  # read and write Speex files
517
518  def read_speex(filename)
519    wavfile = filename + ".wav"
520    File.unlink(wavfile) if File.exist?(wavfile)
521    system(format("speexdec %s %s", filename, wavfile))
522    wavfile
523  end
524
525  def write_speex(snd)
526    if edits(snd)[0] > 0 or header_type(snd) != Mus_riff
527      file = file_name(snd) + ".wav"
528      spxfile = file_name(snd) + "spx"
529      save_sound_as(file, snd, :header_type, Mus_riff)
530      system(format("speexenc %s %s", file, spxfile))
531      File.unlink(file)
532    else
533      system(format("speexenc %s %s", file_name(snd), spxfile))
534    end
535  end
536
537  # read and write FLAC files
538
539  def read_flac(filename)
540    system(format("flac -d %s", filename))
541  end
542
543  def write_flac(snd)
544    if edits(snd)[0] > 0 or header_type(snd) != Mus_riff
545      file = file_name(snd) + ".wav"
546      save_sound_as(file, snd, :header_type, Mus_riff)
547      system(format("flac %s", file))
548      File.unlink(file)
549    else
550      system(format("flac %s ", file_name(snd)))
551    end
552  end
553
554  # read ASCII files
555  #
556  # these are used by Octave (WaveLab) -- each line has one integer,
557  # apparently a signed short.
558
559  def read_ascii(in_filename,
560                 out_filename = "test.snd",
561                 out_type = Mus_next,
562                 out_format = Mus_bshort,
563                 out_srate = 44100)
564    in_buffer = IO.readlines(in_filename)         # array of strings
565    com = format("created by %s: %s", get_func_name, in_filename)
566    out_snd = new_sound(out_filename, 1, out_srate, out_format, out_type, com)
567    bufsize = 512
568    data = make_vct(bufsize)
569    loc = 0
570    frame = 0
571    short2float = 1.0 / 32768.0
572    as_one_edit_rb do | |
573      in_buffer.each do |line|
574        line.split.each do |str_val|
575          val = eval(str_val)
576          data[loc] = val * short2float
577          loc += 1
578          if loc == bufsize
579            vct2channel(data, frame, bufsize, out_snd, 0)
580            frame += bufsize
581            loc = 0
582          end
583        end
584      end
585      if loc > 0
586        vct2channel(data, frame, loc, out_snd, 0)
587      end
588    end
589    out_snd
590  end
591
592  # make dot size dependent on number of samples being displayed
593  #
594  # this could be extended to set time_graph_style to Graph_lines if
595  # many samples are displayed, etc
596
597  add_help(:auto_dot,
598           "auto_dot(snd, chn, y0, y1)  \
599Sets the dot size depending on the number \
600of samples being displayed (use with $graph_hook):
601$graph_hook.add_hook!(\"auto-dot\") do |snd, chn, y0, y1|
602  auto_dot(snd, chn, y0, y1)
603end")
604  def auto_dot(snd, chn, y0, y1)
605    dots = right_sample(snd, chn) - left_sample(snd, chn)
606    case dots
607    when 100
608      set_dot_size(1, snd, chn)
609    when 50
610      set_dot_size(2, snd, chn)
611    when 25
612      set_dot_size(3, snd, chn)
613    else
614      set_dot_size(5, snd, chn)
615    end
616    false
617  end
618
619  # move window left edge to mark upon 'm'
620  #
621  # in large sounds, it can be pain to get the left edge of the window
622  # aligned with a specific spot in the sound.  In this code, we
623  # assume the desired left edge has a mark, and the 'm' key (without
624  # control) will move the window left edge to that mark.
625
626  add_help(:first_mark_in_window_at_left,
627           "first_mark_in_window_at_left()  \
628Moves the graph so that the leftmost visible mark is at the left edge:
629bind_key(?m, 0, lambda do | | first_mark_in_window_at_left end)")
630  def first_mark_in_window_at_left
631    keysnd = Snd.snd
632    keychn = Snd.chn
633    current_left_sample = left_sample(keysnd, keychn)
634    chan_marks = marks(keysnd, keychn)
635    if chan_marks.null?
636      snd_print("no marks!")
637    else
638      leftmost = chan_marks.map do |m|
639        mark_sample(m)
640      end.detect do |m|
641        m > current_left_sample
642      end
643      if leftmost.null?
644        snd_print("no mark in window")
645      else
646        set_left_sample(leftmost, keysnd, keychn)
647        Keyboard_no_action
648      end
649    end
650  end
651
652  # flash selected data red and green
653
654  add_help(:flash_selected_data,
655           "flash_selected_data(millisecs)  \
656Causes the selected data to flash red and green.")
657  def flash_selected_data(interval)
658    if selected_sound
659      set_selected_data_color(selected_data_color == Red ? Green : Red)
660      call_in(interval, lambda do | | flash_selected_data(interval) end)
661    end
662  end
663
664  # use loop info (if any) to set marks at loop points
665
666  add_help(:mark_loops,
667           "mark_loops()  \
668Places marks at loop points found in the selected sound's header.")
669  def mark_loops
670    loops = (sound_loop_info or mus_sound_loop_info(file_name))
671    if loops and !loops.empty?
672      unless loops[0].zero? and loops[1].zero?
673        add_mark(loops[0])
674        add_mark(loops[1])
675        unless loops[2].zero? and loops[3].zero?
676          add_mark(loops[2])
677          add_mark(loops[3])
678        end
679      end
680    else
681      Snd.display("%s has no loop info", short_file_name.inspect)
682    end
683  end
684
685  # mapping extensions (map arbitrary single-channel function over
686  # various channel collections)
687
688  add_help(:do_all_chans,
689           "do_all_chans(edhist) do |y| ... end  \
690Applies func to all active channels, \
691using EDHIST as the edit history indication: \
692do_all_chans(\"double all samples\", do |val| 2.0 * val end)")
693  def do_all_chans(origin, &func)
694    Snd.sounds.each do |snd|
695      channels(snd).times do |chn|
696        map_channel(func, 0, false, snd, chn, false, origin)
697      end
698    end
699  end
700
701  add_help(:update_graphs,
702           "update_graphs()  \
703Updates (redraws) all graphs.")
704  def update_graphs
705    Snd.sounds.each do |snd|
706      channels(snd).times do |chn|
707        update_time_graph(snd, chn)
708      end
709    end
710  end
711
712  add_help(:do_chans,
713           "do_chans(edhist) do |y| ... end  \
714Applies func to all sync'd channels using EDHIST \
715as the edit history indication.")
716  def do_chans(*origin, &func)
717    snc = sync
718    if snc > 0
719      Snd.sounds.each do |snd|
720        channels(snd).times do |chn|
721          if sync(snd) == snc
722            map_channel(func, 0, false, snd, chn, false,
723                        (origin.empty? ? "" : format(*origin)))
724          end
725        end
726      end
727    else
728      snd_warning("sync not set")
729    end
730  end
731
732  add_help(:do_sound_chans,
733           "do_sound_chans(edhist) do |y| ... end  \
734Applies func to all selected channels using EDHIST \
735as the edit history indication.")
736  def do_sound_chans(*origin, &func)
737    if snd = selected_sound
738      channels(snd).times do |chn|
739        map_channel(func, 0, false, snd, chn, false,
740                    (origin.empty? ? "" : format(*origin)))
741      end
742    else
743      snd_warning("no selected sound")
744    end
745  end
746
747  add_help(:every_sample?,
748           "every_sample? do |y| ... end  \
749Returns true if func is not false for all samples in the current channel, \
750otherwise it moves the cursor to the first offending sample.")
751  def every_sample?(&func)
752    snd = Snd.snd
753    chn = Snd.chn
754    if baddy = scan_channel(lambda do |y|
755                              (not func.call(y))
756                            end, 0, framples(snd, chn), snd, chn)
757      set_cursor(baddy[1])
758    end
759    (not baddy)
760  end
761
762  add_help(:sort_samples,
763           "sort_samples(bins)  \
764Provides a histogram in BINS bins.")
765  def sort_samples(nbins)
766    bins = make_array(nbins, 0)
767    scan_channel(lambda do |y|
768                   bin = (y.abs * nbins).floor
769                   bins[bin] += 1
770                   false
771                 end)
772    bins
773  end
774
775  # mix mono sound into stereo sound panning according to env
776
777  add_help(:place_sound,
778           "place_sound(mono_snd, stereo_snd, pan_env)  \
779Mixes a mono sound into a stereo sound, splitting it into two copies \
780whose amplitudes depend on the envelope PAN_ENV.  \
781If PAN_ENV is a number, \
782the sound is split such that 0 is all in channel 0 \
783and 90 is all in channel 1.")
784  def place_sound(mono_snd, stereo_snd, pan_env)
785    len = framples(mono_snd)
786    if number?(pan_env)
787      pos = pan_env / 90.0
788      rd0 = make_sampler(0, mono_snd, false)
789      rd1 = make_sampler(0, mono_snd, false)
790      map_channel(lambda do |y| y + pos * read_sample(rd1) end,
791                  0, len, stereo_snd, 1)
792      map_channel(lambda do |y| y + (1.0 - pos) * read_sample(rd0) end,
793                  0, len, stereo_snd, 0)
794    else
795      e0 = make_env(:envelope, pan_env, :length, len)
796      e1 = make_env(:envelope, pan_env, :length, len)
797      rd0 = make_sampler(0, mono_snd, false)
798      rd1 = make_sampler(0, mono_snd, false)
799      map_channel(lambda do |y| y + env(e1) * read_sample(rd1) end,
800                  0, len, stereo_snd, 1)
801      map_channel(lambda do |y| y + (1.0 - env(e0)) * read_sample(rd0) end,
802                  0, len, stereo_snd, 0)
803    end
804  end
805
806  # FFT-based editing
807
808  add_help(:fft_edit,
809           "fft_edit(low_Hz, high_Hz)  \
810Ffts an entire sound, removes all energy below low-Hz and all above high-Hz, \
811then inverse ffts.")
812  def fft_edit(bottom, top, snd = false, chn = false)
813    sr = srate(snd).to_f
814    len = framples(snd, chn)
815    fsize = (2.0 ** (log(len) / log(2.0)).ceil).to_i
816    rdata = channel2vct(0, fsize, snd, chn)
817    idata = make_vct(fsize)
818    lo = (bottom / (sr / fsize)).round
819    hi = (top / (sr / fsize)).round
820    fft(rdata, idata, 1)
821    j = fsize - 1
822    lo.times do |i|
823      rdata[i] = rdata[j] = 0.0
824      rdata[i] = rdata[j] = 0.0
825      j -= 1
826    end
827    j = fsize - hi
828    (hi..(fsize / 2)).each do |i|
829      rdata[i] = rdata[j] = 0.0
830      rdata[i] = rdata[j] = 0.0
831      j -= 1
832    end
833    fft(rdata, idata, -1)
834    vct_scale!(rdata, 1.0 / fsize)
835    vct2channel(rdata, 0, len - 1, snd, chn, false,
836                format("%s(%s, %s", get_func_name, bottom, top))
837  end
838
839  add_help(:fft_squelch,
840           "fft_squelch(squelch, snd=false, chn=false)  \
841Ffts an entire sound, sets all bins to 0.0 whose energy is below squelch, \
842then inverse ffts.")
843  def fft_squelch(squelch, snd = false, chn = false)
844    len = framples(snd, chn)
845    fsize = (2.0 ** (log(len) / log(2.0)).ceil).to_i
846    rdata = channel2vct(0, fsize, snd, chn)
847    idata = make_vct(fsize)
848    fsize2 = fsize / 2
849    fft(rdata, idata, 1)
850    vr = vct_copy(rdata)
851    vi = vct_copy(idata)
852    rectangular2polar(vr, vi)
853    scaler = vct_peak(vr)
854    scl_squelch = squelch * scaler
855    j = fsize - 1
856    fsize2.times do |i|
857      if sqrt(rdata[i] * rdata[i] + idata[i] * idata[i]) < scl_squelch
858        rdata[i] = rdata[j] = 0.0
859        rdata[i] = rdata[j] = 0.0
860      end
861      j -= 1
862    end
863    fft(rdata, idata, -1)
864    vct_scale!(rdata, 1.0 / fsize)
865    vct2channel(rdata, 0, len - 1, snd, chn, false,
866                format("%s(%s", get_func_name, squelch))
867    scaler
868  end
869
870  add_help(:fft_cancel,
871           "fft_cancel(lo_freq, hi_freq, snd=false, chn=false)  \
872Ffts an entire sound, sets the bin(s) representing lo_freq to hi_freq to 0.0, \
873then inverse ffts.")
874  def fft_cancel(lo_freq, hi_freq, snd = false, chn = false)
875    sr = srate(snd).to_f
876    len = framples(snd, chn)
877    fsize = (2.0 ** (log(len) / log(2.0)).ceil).to_i
878    rdata = channel2vct(0, fsize, snd, chn)
879    idata = make_vct(fsize)
880    fsize2 = fsize / 2
881    fft(rdata, idata, 1)
882    hz_bin = sr / fsize
883    lo_bin = (lo_freq / hz_bin).round
884    hi_bin = (hi_freq / hz_bin).round
885    j = fsize - lo_bin - 1
886    fsize2.times do |i|
887      if i > hi_bin
888        rdata[i] = rdata[j] = 0.0
889        rdata[i] = rdata[j] = 0.0
890      end
891      j -= 1
892    end
893    fft(rdata, idata, -1)
894    vct_scale!(rdata, 1.0 / fsize)
895    vct2channel(rdata, 0, len - 1, snd, chn, false,
896                format("%s(%s, %s", get_func_name, lo_freq, hi_freq))
897  end
898
899  # same idea but used to distinguish vowels (steady-state) from consonants
900
901  class Ramp < Musgen
902    def initialize(size = 128)
903      super()
904      @val = 0.0
905      @size = size
906      @incr = 1.0 / size
907      @up = true
908    end
909
910    def inspect
911      format("%s.new(%d)", self.class, @size)
912    end
913
914    def to_s
915      format("#<%s size: %d, val: %1.3f, incr: %1.3f, up: %s>",
916             self.class, @size, @val, @incr, @up)
917    end
918
919    def run_func(up = true, dummy = false)
920      @up = up
921      @val += (@up ? @incr : -@incr)
922      @val = [1.0, [0.0, @val].max].min
923    end
924
925    def run(up = true)
926      self.run_func(up, false)
927    end
928  end
929
930  add_help(:make_ramp,
931           "make_ramp(size=128)  \
932Returns a ramp generator.")
933  def make_ramp(size = 128)
934    Ramp.new(size)
935  end
936
937  add_help(:ramp,
938           "ramp(gen, up)  \
939Is a kind of CLM generator that produces a ramp of a given length, \
940then sticks at 0.0 or 1.0 until the UP argument changes.")
941  def ramp(gen, up)
942    gen.run_func(up, false)
943  end
944
945  add_help(:squelch_vowels,
946           "squelch_vowels(snd=false, chn=false)  \
947Suppresses portions of a sound that look like steady-state.")
948  def squelch_vowels(snd = false, chn = false)
949    fft_size = 32
950    fft_mid = (fft_size / 2.0).floor
951    ramper = Ramp.new(256)
952    peak = maxamp(snd, chn) / fft_mid
953    read_ahead = make_sampler(0, snd, chn)
954    rl = Vct.new(fft_size) do
955      read_sample(read_ahead)
956    end
957    im = Vct.new(fft_size, 0.0)
958    ctr = fft_size - 1
959    in_vowel = false
960    map_channel(lambda do |y|
961                  ctr += 1
962                  if ctr == fft_size
963                    ctr = 0
964                    fft(rl, im, 1)
965                    rl.multiply!(rl)
966                    im.multiply!(im)
967                    rl.add!(im)
968                    im.fill(0.0)
969                    in_vowel = (rl[0] + rl[1] + rl[2] + rl[3]) > peak
970                    rl.map! do
971                      read_sample(read_ahead)
972                    end
973                  end
974                  y * (1.0 - ramper.run(in_vowel))
975                end, 0, false, snd, chn, false, "squelch_vowels(")
976  end
977
978  add_help(:fft_env_data,
979           "fft_env_data(fft-env, snd=false, chn=false)  \
980Applies fft_env as spectral env to current sound, returning vct of new data.")
981  def fft_env_data(fft_env, snd = false, chn = false)
982    len = framples(snd, chn)
983    fsize = (2 ** (log(len) / log(2.0)).ceil).to_i
984    rdata = channel2vct(0, fsize, snd, chn)
985    idata = make_vct(fsize)
986    fsize2 = fsize / 2
987    en = make_env(:envelope, fft_env, :length, fsize2)
988    fft(rdata, idata, 1)
989    j = fsize - 1
990    fsize2.times do |i|
991      val = env(en)
992      rdata[i] *= val
993      idata[i] *= val
994      rdata[j] *= val
995      idata[j] *= val
996      j -= 1
997    end
998    fft(rdata, idata, -1)
999    vct_scale!(rdata, 1.0 / fsize)
1000  end
1001
1002  add_help(:fft_env_edit,
1003           "fft_env_edit(fft-env, snd=false, chn=false)  \
1004Edits (filters) current chan using fft_env.")
1005  def fft_env_edit(fft_env, snd = false, chn = false)
1006    vct2channel(fft_env_data(fft_env, snd, chn), 0, framples(snd, chn) - 1,
1007                snd, chn, false,
1008                format("%s(%s", get_func_name, fft_env.inspect))
1009  end
1010
1011  add_help(:fft_env_interp,
1012           "fft_env_interp(env1, env2, interp, snd=false, chn=false)  \
1013Interpolates between two fft-filtered \
1014versions (env1 and env2 are the spectral envelopes) \
1015following interp (an env between 0 and 1).")
1016  def fft_env_interp(env1, env2, interp, snd = false, chn = false)
1017    data1 = fft_env_data(env1, snd, chn)
1018    data2 = fft_env_data(env2, snd, chn)
1019    len = framples(snd, chn)
1020    en = make_env(:envelope, interp, :length, len)
1021    new_data = make_vct!(len) do |i|
1022      pan = env(en)
1023      (1.0 - pan) * data1[i] + pan * data2[i]
1024    end
1025    vct2channel(new_data, 0, len - 1, snd, chn, false,
1026                format("%s(%s, %s, %s", get_func_name,
1027                       env1.inspect, env2.inspect, interp.inspect))
1028  end
1029
1030  add_help(:fft_smoother,
1031           "fft_smoother(cutoff, start, samps, snd=false, chn=false)  \
1032Uses fft-filtering to smooth a section: \
1033vct2channel(fft_smoother(0.1, cursor, 400, 0, 0), cursor, 400)")
1034  def fft_smoother(cutoff, start, samps, snd = false, chn = false)
1035    fftpts = (2 ** (log(samps + 1) / log(2.0)).ceil).to_i
1036    rl = channel2vct(start, samps, snd, chn)
1037    im = make_vct(fftpts)
1038    top = (fftpts * cutoff.to_f).floor
1039    old0 = rl[0]
1040    old1 = rl[samps - 1]
1041    oldmax = vct_peak(rl)
1042    fft(rl, im, 1)
1043    (top...fftpts).each do |i| rl[i] = im[i] = 0.0 end
1044    fft(rl, im, -1)
1045    vct_scale!(rl, 1.0 / fftpts)
1046    newmax = vct_peak(rl)
1047    if newmax.zero?
1048      rl
1049    else
1050      if (scl = oldmax / newmax) > 1.5
1051        vct_scale!(rl, scl)
1052      end
1053      new0 = rl[0]
1054      new1 = rl[samps - 1]
1055      offset0 = old0 - new0
1056      offset1 = old1 - new1
1057      incr = offset0 == offset1 ? 0.0 : ((offset1 - offset0) / samps)
1058      trend = offset0 - incr
1059      rl.map do |val|
1060        trend += incr
1061        val += trend
1062      end
1063    end
1064  end
1065
1066  # comb-filter
1067
1068  add_help(:comb_filter,
1069           "comb_filter(scaler, size)  \
1070Returns a comb-filter ready for map_channel etc: \
1071map_channel(comb_filter(0.8, 32))  \
1072If you're in a hurry use: clm_channel(make_comb(0.8, 32)) instead.")
1073  def comb_filter(scaler, size)
1074    cmb = make_comb(scaler, size)
1075    lambda do |inval| comb(cmb, inval) end
1076  end
1077
1078  # by using filters at harmonically related sizes, we can get chords:
1079
1080  add_help(:comb_chord,
1081           "comb_chord(scl, size, amp, interval_one=0.75, interval_two=1.2)  \
1082Returns a set of harmonically-related comb filters: \
1083map_channel(comb_chord(0.95, 100, 0.3))")
1084  def comb_chord(scaler, size, amp, interval_one = 0.75, interval_two = 1.2)
1085    c1 = make_comb(scaler, size.to_i)
1086    c2 = make_comb(scaler, (interval_one * size).to_i)
1087    c3 = make_comb(scaler, (interval_two * size).to_i)
1088    lambda do |inval|
1089      amp * (comb(c1, inval) + comb(c2, inval) + comb(c3, inval))
1090    end
1091  end
1092
1093  # or change the comb length via an envelope:
1094
1095  add_help(:zcomb,
1096           "zcomb(scaler, size, pm)  \
1097Returns a comb filter whose length varies according to an envelope: \
1098map_channel(zcomb(0.8, 32, [0, 0, 1, 10]))")
1099  def zcomb(scaler, size, pm)
1100    max_envelope_1 = lambda do |en, mx|
1101      1.step(en.length - 1, 2) do |i| mx = [mx, en[i]].max.to_f end
1102      mx
1103    end
1104    cmb = make_comb(scaler, size,
1105                    :max_size, (max_envelope_1.call(pm, 0.0) + size + 1).to_i)
1106    penv = make_env(:envelope, pm, :length, framples())
1107    lambda do |inval| comb(cmb, inval, env(penv)) end
1108  end
1109
1110  add_help(:notch_filter,
1111           "notch_filter(scaler, size)  \
1112Returns a notch-filter: map_channel(notch_filter(0.8, 32))")
1113  def notch_filter(scaler, size)
1114    gen = make_notch(scaler, size)
1115    lambda do |inval| notch(gen, inval) end
1116  end
1117
1118  add_help(:formant_filter,
1119                   "formant_filter(radius, frequency)  \
1120Returns a formant generator: map_channel(formant_filter(0.99, 2400))  \
1121Faster is: filter_sound(make_formant(2400, 0.99))")
1122  def formant_filter(radius, freq)
1123    frm = make_formant(radius, freq)
1124    lambda do |inval| formant(frm, inval) end
1125  end
1126
1127  # to impose several formants, just add them in parallel:
1128
1129  add_help(:formants,
1130           "formants(r1, f1, r2, f2, r3, f3)  \
1131Returns 3 formant filters in parallel: \
1132map_channel(formants(0.99, 900, 0.98, 1800, 0.99 2700))")
1133  def formants(r1, f1, r2, f2, r3, f3)
1134    fr1 = make_formant(f1, r1)
1135    fr2 = make_formant(f2, r2)
1136    fr3 = make_formant(f3, r3)
1137    lambda do |inval|
1138      formant(fr1, inval) + formant(fr2, inval) + formant(fr3, inval)
1139    end
1140  end
1141
1142  add_help(:moving_formant,
1143           "moving_formant(radius, move)  \
1144Returns a time-varying (in frequency) formant filter: \
1145map_channel(moving_formant(0.99, [0, 1200, 1, 2400]))")
1146  def moving_formant(radius, move)
1147    frm = make_formant(move[1], radius)
1148    menv = make_env(:envelope, move, :length, framples())
1149    lambda do |inval|
1150      val = formant(frm, inval)
1151      frm.frequency = env(menv)
1152      val
1153    end
1154  end
1155
1156  add_help(:osc_formants,
1157           "osc_formants(radius, bases, amounts, freqs)  \
1158Set up any number of independently oscillating formants, \
1159then calls map_channel: \
1160osc_formants(0.99, vct(400, 800, 1200), vct(400, 800, 1200), vct(4, 2, 3))")
1161  def osc_formants(radius, bases, amounts, freqs)
1162    len = bases.length
1163    frms = make_array(len) do |i| make_formant(bases[i], radius) end
1164    oscs = make_array(len) do |i| make_oscil(freqs[i]) end
1165    map_channel_rb() do |x|
1166      val = 0.0
1167      frms.each_with_index do |frm, i|
1168        val += formant(frm, x)
1169        set_mus_frequency(frm, bases[i] + amounts[i] * oscil(oscs[i]))
1170      end
1171      val
1172    end
1173  end
1174
1175  # echo
1176
1177  add_help(:echo,
1178           "echo(scaler, secs)  \
1179Returns an echo maker: map_channel(echo(0.5, 0.5), 0 44100)")
1180  def echo(scaler, secs)
1181    del = make_delay((secs * srate()).round)
1182    lambda do |inval|
1183      inval + delay(del, scaler * (tap(del) + inval))
1184    end
1185  end
1186
1187  add_help(:zecho,
1188           "zecho(scaler, secs, freq, amp)  \
1189Returns a modulated echo maker: \
1190map_channel(zecho(0.5, 0.75, 6, 10.0), 0, 65000)")
1191  def zecho(scaler, secs, freq, amp)
1192    os = make_oscil(freq)
1193    len = (secs * srate()).round
1194    del = make_delay(len, :max_size, (len + amp + 1).to_i)
1195    lambda do |inval|
1196      inval + delay(del, scaler * (tap(del) + inval), amp * oscil(os))
1197    end
1198  end
1199
1200  add_help(:flecho,
1201           "flecho(scaler, secs)  \
1202Returns a low-pass filtered echo maker: \
1203map_channel(flecho(0.5, 0.9), 0, 75000)" )
1204  def flecho(scaler, secs)
1205    flt = make_fir_filter(:order, 4, :xcoeffs, vct(0.125, 0.25, 0.25, 0.125))
1206    del = make_delay((secs * srate()).round)
1207    lambda do |inval|
1208      inval + delay(del, fir_filter(flt, scaler * (tap(del) + inval)))
1209    end
1210  end
1211
1212  # ring-mod and am
1213  #
1214  # CLM instrument is ring-modulate.ins
1215
1216  add_help(:ring_mod,
1217           "ring_mod(freq, gliss_env)  \
1218Returns a time-varying ring-modulation filter: \
1219map_channel(ring_mod(10, [0, 0, 1, hz2radians(100)]))")
1220  def ring_mod(freq, gliss_env)
1221    os = make_oscil(:frequency, freq)
1222    len = framples()
1223    genv = make_env(:envelope, gliss_env, :length, len)
1224    lambda do |inval| oscil(os, env(genv)) * inval end
1225  end
1226
1227  add_help(:am,
1228           "am(freq)  \
1229returns an amplitude-modulator: map_channel(am(440))")
1230  def am(freq)
1231    os = make_oscil(freq)
1232    lambda do |inval| amplitude_modulate(1.0, inval, oscil(os)) end
1233  end
1234
1235  def vibro(speed, depth)
1236    sine = make_oscil(speed)
1237    scl = 0.5 * depth
1238    offset = 1.0 - scl
1239    lambda do |inval| inval * (offset + scl * oscil(sine)) end
1240  end
1241
1242  # hello-dentist
1243  #
1244  # CLM instrument version is in clm.html
1245
1246  add_help(:hello_dentist,
1247           "hello_dentist(frq, amp, snd=false, chn=false)  \
1248Varies the sampling rate randomly, \
1249making a voice sound quavery: hello_dentist(40.0, 0.1)")
1250  def hello_dentist(freq, amp, snd = false, chn = false)
1251    rn = make_rand_interp(:frequency, freq, :amplitude, amp)
1252    i = 0
1253    len = framples()
1254    in_data = channel2vct(0, len, snd, chn)
1255    out_len = (len * (1.0 + 2.0 * amp)).to_i
1256    out_data = make_vct(out_len)
1257    rd = make_src(:srate, 1.0,
1258                  :input, lambda do |dir|
1259                    val = i.between?(0, len - 1) ? in_data[i] : 0.0
1260                    i += dir
1261                    val
1262                  end)
1263    vct2channel(out_data.map do |x| src(rd, rand_interp(rn)) end,
1264                0, len, snd, chn, false,
1265                format("%s(%s, %s", get_func_name, freq, amp))
1266  end
1267
1268  # a very similar function uses oscil instead of rand-interp, giving
1269  # various "Forbidden Planet" sound effects:
1270
1271  add_help(:fp,
1272           "fp(sr, osamp, osfrq, snd=false, chn=false)  \
1273Varies the sampling rate via an oscil: fp(1.0, 0.3, 20)")
1274  def fp(sr, osamp, osfreq, snd = false, chn = false)
1275    os = make_oscil(:frequency, osfreq)
1276    s = make_src(:srate, sr)
1277    len = framples(snd, chn)
1278    sf = make_sampler(0, snd, chn)
1279    out_data = make_vct!(len) do
1280      src(s, osamp * oscil(os),
1281          lambda do |dir|
1282            dir > 0 ? next_sample(sf) : previous_sample(sf)
1283          end)
1284    end
1285    free_sampler(sf)
1286    vct2channel(out_data, 0, len, snd, chn, false,
1287                format("%s(%s, %s, %s", get_func_name, sr, osamp, osfreq))
1288  end
1289
1290  # compand
1291
1292  Compand_table = vct(-1.0, -0.96, -0.9, -0.82, -0.72, -0.6, -0.45, -0.25,
1293                       0.0, 0.25, 0.45, 0.6, 0.72, 0.82, 0.9, 0.96, 1.0)
1294  add_help(:compand,
1295           "compand()  \
1296Returns a compander: map_channel(compand())")
1297  def compand()
1298    lambda do |inval|
1299      array_interp(Compand_table, 8.0 + 8.0 * inval, Compand_table.length)
1300    end
1301  end
1302
1303  # shift pitch keeping duration constant
1304  #
1305  # both src and granulate take a function argument to get input
1306  # whenever it is needed.  in this case, src calls granulate which
1307  # reads the currently selected file.  CLM version is in expsrc.ins
1308
1309  add_help(:expsrc,
1310           "expsrc(rate, snd=false, chn=false)  \
1311Uses sampling-rate conversion and granular synthesis to produce a sound \
1312at a new pitch but at the original tempo.  \
1313It returns a function for map_chan.")
1314  def expsrc(rate, snd = false, chn = false)
1315    gr = make_granulate(:expansion, rate)
1316    sr = make_src(:srate, rate)
1317    vsize = 1024
1318    vbeg = 0
1319    v = channel2vct(0, vsize)
1320    inctr = 0
1321    lambda do |inval|
1322      src(sr, 0.0, lambda do |dir|
1323            granulate(gr, lambda do |dr|
1324                        val = v[inctr]
1325                        inctr += dr
1326                        if inctr >= vsize
1327                          vbeg += inctr
1328                          inctr = 0
1329                          v = channel2vct(vbeg, vsize, snd, chn)
1330                        end
1331                        val
1332                      end)
1333          end)
1334    end
1335  end
1336
1337  # the next (expsnd) changes the tempo according to an envelope; the
1338  # new duration will depend on the expansion envelope -- we integrate
1339  # it to get the overall expansion, then use that to decide the new
1340  # length.
1341
1342  add_help(:expsnd,
1343           "expsnd(gr_env, snd=false, chn=false)  \
1344Uses the granulate generator to change tempo \
1345according to an envelope: expsnd([0, 0.5, 2, 2.0])")
1346  def expsnd(gr_env, snd = false, chn = false)
1347    dur = ((framples(snd, chn) / srate(snd)) *
1348           integrate_envelope(gr_env) / envelope_last_x(gr_env))
1349    gr = make_granulate(:expansion, gr_env[1], :jitter, 0)
1350    ge = make_env(:envelope, gr_env, :duration, dur)
1351    sound_len = (srate(snd) * dur).to_i
1352    len = [sound_len, framples(snd, chn)].max
1353    sf = make_sampler(0, snd, chn)
1354    out_data = make_vct!(len) do
1355      val = granulate(gr, lambda do |dir| next_sample(sf) end)
1356      gr.increment = env(ge)
1357      val
1358    end
1359    free_sampler(sf)
1360    vct2channel(out_data, 0, len, snd, chn, false,
1361                format("%s(%s", get_func_name, gr_env.inspect))
1362  end
1363
1364  # cross-synthesis
1365  #
1366  # CLM version is in clm.html
1367
1368  add_help(:cross_synthesis,
1369           "cross_synthesis(cross_snd, amp, fftsize, r)  \
1370Does cross-synthesis between CROSS_SND (a sound index) \
1371and the currently selected sound: \
1372map_channel(cross_synthesis(1, 0.5, 128, 6.0))")
1373  def cross_synthesis(cross_snd, amp, fftsize, r)
1374    freq_inc = fftsize / 2
1375    fdr = make_vct(fftsize)
1376    fdi = make_vct(fftsize)
1377    spectr = make_vct(freq_inc)
1378    inctr = 0
1379    ctr = freq_inc
1380    radius = 1.0 - r / fftsize.to_f
1381    osr = mus_srate()
1382    csr = srate()
1383    bin = csr / fftsize
1384    # The comment from examp.scm applies of course here as well:
1385    # ;; if mus-srate is 44.1k and srate is 48k, make-formant
1386    # ;;    thinks we're trying to go past srate/2
1387    # ;;    and in any case it's setting its formants incorrectly
1388    # ;;    for the actual output srate
1389    # begin of temporary mus-srate
1390    set_mus_srate(csr)
1391    fmts = make_array(freq_inc) do |i|
1392      make_formant(i * bin, radius)
1393    end
1394    set_mus_srate(osr)
1395    # end of temporary mus-srate
1396    formants = make_formant_bank(fmts, spectr)
1397    lambda do |inval|
1398      if ctr == freq_inc
1399        fdr = channel2vct(inctr, fftsize, cross_snd, 0)
1400        inctr += freq_inc
1401        spectrum(fdr, fdi, false, 2)
1402        vct_subtract!(fdr, spectr)
1403        vct_scale!(fdr, 1.0 / freq_inc)
1404        ctr = 0
1405      end
1406      ctr += 1
1407      vct_add!(spectr, fdr)
1408      amp * formant_bank(formants, inval)
1409    end
1410  end
1411
1412  # similar ideas can be used for spectral cross-fades, etc -- for example:
1413
1414  add_help(:voiced2unvoiced,
1415           "voiced2unvoiced(amp, fftsize, r, tempo, snd=false, chn=false)  \
1416Turns a vocal sound into whispering: voiced2unvoiced(1.0, 256, 2.0, 2.0)")
1417  def voiced2unvoiced(amp, fftsize, r, tempo, snd = false, chn = false)
1418    freq_inc = fftsize / 2
1419    fdr = make_vct(fftsize)
1420    fdi = make_vct(fftsize)
1421    spectr = make_vct(freq_inc)
1422    noi = make_rand(:frequency, srate(snd) / 3.0)
1423    inctr = 0
1424    ctr = freq_inc
1425    osr = mus_srate()
1426    csr = srate()
1427    # See cross_synthesis above.
1428    # begin of temporary mus-srate
1429    set_mus_srate(csr)
1430    radius = 1.0 - r.to_f / fftsize
1431    bin = csr / fftsize
1432    len = framples(snd, chn)
1433    outlen = (len / tempo).floor
1434    hop = (freq_inc * tempo).floor
1435    out_data = make_vct([len, outlen].max)
1436    fmts = make_array(freq_inc) do |i|
1437      make_formant(i * bin, radius)
1438    end
1439    set_mus_srate(osr)
1440    # end of temporary mus-srate
1441    formants = make_formant_bank(fmts, spectr)
1442    old_peak_amp = new_peak_amp = 0.0
1443    outlen.times do |i|
1444      if ctr == freq_inc
1445        fdr = channel2vct(inctr, fftsize, snd, chn)
1446        if (pk = vct_peak(fdr)) > old_peak_amp
1447          old_peak_amp = pk
1448        end
1449        spectrum(fdr, fdi, false, 2)
1450        inctr += hop
1451        vct_subtract!(fdr, spectr)
1452        vct_scale!(fdr, 1.0 / freq_inc)
1453        ctr = 0
1454      end
1455      ctr += 1
1456      vct_add!(spectr, fdr)
1457      if (outval = formant_bank(formants, rand(noi))).abs > new_peak_amp
1458        new_peak_amp = outval.abs
1459      end
1460      out_data[i] = outval
1461    end
1462    vct_scale!(out_data, amp * (old_peak_amp / new_peak_amp))
1463    vct2channel(out_data, 0, [len, outlen].max, snd, chn, false,
1464                format("%s(%s, %s, %s, %s", get_func_name,
1465                       amp, fftsize, r, tempo))
1466  end
1467
1468  # very similar but use sum-of-cosines (glottal pulse train?)
1469  # instead of white noise
1470
1471  add_help(:pulse_voice,
1472           "pulse_voice(cosin, freq=440.0, amp=1.0, fftsize=256, r=2.0, \
1473snd=false, chn=false)  \
1474Use ncos to manipulate speech sounds.")
1475  def pulse_voice(cosin, freq = 440.0, amp = 1.0,
1476                  fftsize = 256, r = 2.0, snd = false, chn = false)
1477    freq_inc = fftsize / 2
1478    spectr = make_vct(freq_inc)
1479    len = framples(snd, chn)
1480    osr = mus_srate()
1481    csr = srate(snd)
1482    # See cross_synthesis above.
1483    # begin of temporary mus-srate
1484    set_mus_srate(csr)
1485    bin = csr / fftsize
1486    radius = 1.0 - (r.to_f / fftsize)
1487    fmts = make_array(freq_inc) do |i|
1488      make_formant(:radius, radius, :frequency, i * bin)
1489    end
1490    formants = make_formant_bank(fmts, spectr)
1491    set_mus_srate(osr)
1492    # end of temporary mus-srate
1493    old_peak_amp = 0.0
1494    pulse = make_ncos(freq, cosin)
1495    fdr = nil
1496    inctr = 0
1497    fdi = make_vct(fftsize)
1498    inv_freq_inc = 1.0 / freq_inc
1499    out_data = make_vct!(len) do |i|
1500      if i.modulo(freq_inc) == 0
1501        fdr = channel2vct(inctr, fftsize, snd, chn)
1502        old_peak_amp = [vct_peak(fdr), old_peak_amp].max
1503        spectrum(fdr, fdi, false, 2)
1504        vct_subtract!(fdr, spectr)
1505        vct_scale!(fdr, inv_freq_inc)
1506        inctr += freq_inc
1507      end
1508      vct_add!(spectr, fdr)
1509      formant_bank(formants, ncos(pulse))
1510    end
1511    vct_scale!(out_data, (old_peak_amp / vct_peak(out_data)) * amp)
1512    vct2channel(out_data, 0, len, snd, chn)
1513  end
1514  # pulse_voice(80,   20.0, 1.0, 1024, 0.01)
1515  # pulse_voice(80,  120.0, 1.0, 1024, 0.2)
1516  # pulse_voice(30,  240.0, 1.0, 1024, 0.1)
1517  # pulse_voice(30,  240.0, 1.0, 2048)
1518  # pulse_voice( 6, 1000.0, 1.0,  512)
1519
1520  # convolution example
1521
1522  add_help(:cnvtest,
1523           "cnvtest(snd0, snd1, amp)  \
1524Convolves snd0 and snd1, scaling by amp, \
1525returns new max amp: cnvtest(0, 1, 0.1)")
1526  def cnvtest(snd0, snd1, amp)
1527    flt_len = framples(snd0)
1528    total_len = flt_len + framples(snd1)
1529    cnv = make_convolve(:filter, channel2vct(0, flt_len, snd0))
1530    sf = make_sampler(0, snd1, false)
1531    out_data = make_vct!(total_len) do
1532      convolve(cnv, lambda do |dir| next_sample(sf) end)
1533    end
1534    free_sampler(sf)
1535    vct_scale!(out_data, amp)
1536    max_samp = vct_peak(out_data)
1537    vct2channel(out_data, 0, total_len, snd1)
1538    if max_samp > 1.0
1539      set_y_bounds(snd1, [-max_samp, max_samp])
1540    end
1541    max_samp
1542  end
1543
1544  # swap selection chans
1545
1546  add_help(:swap_selection_channels,
1547           "swap_selection_channels()  \
1548Swaps the currently selected data's channels.")
1549  def swap_selection_channels
1550    if (not selection?)
1551      Snd.raise(:no_active_selection)
1552    end
1553    if selection_chans != 2
1554      Snd.raise(:wrong_number_of_channels, "need a stereo selection")
1555    end
1556    beg = selection_position()
1557    len = selection_framples()
1558    snd_chn0 = snd_chn1 = nil
1559    Snd.sounds.each do |snd|
1560      channels(snd).times do |chn|
1561        if selection_member?(snd, chn)
1562          if snd_chn0.nil?
1563            snd_chn0 = [snd, chn]
1564          elsif snd_chn1.nil?
1565            snd_chn1 = [snd, chn]
1566            break
1567          end
1568        end
1569      end
1570    end
1571    if snd_chn1.nil?
1572      Snd.raise(:wrong_number_of_channels, "need two channels to swap")
1573    end
1574    swap_channels(snd_chn0[0], snd_chn0[1], snd_chn1[0], snd_chn1[1], beg, len)
1575  end
1576
1577  # sound interp
1578  #
1579  # make-sound-interp sets up a sound reader that reads a channel at
1580  # an arbitary location, interpolating between samples if necessary,
1581  # the corresponding "generator" is sound-interp
1582
1583  add_help(:make_sound_interp,
1584           "make_sound_interp(start, snd=false, chn=false)  \
1585An interpolating reader for SND's channel CHN.")
1586  def make_sound_interp(start, snd = false, chn = false)
1587    data = channel2vct(0, false, snd, chn)
1588    size = data.length
1589    lambda do |loc|
1590      array_interp(data, loc, size)
1591    end
1592  end
1593
1594  add_help(:sound_interp,
1595           "sound_interp(func, loc)  \
1596Sample at LOC (interpolated if necessary) from FUNC \
1597created by make_sound_interp.")
1598  def sound_interp(func, loc)
1599    func.call(loc)
1600  end
1601
1602  def sound_via_sound(snd1, snd2)
1603    intrp = make_sound_interp(0, snd1, 0)
1604    len = framples(snd1, 0) - 1
1605    rd = make_sampler(0, snd2, 0)
1606    mx = maxamp(snd2, 0)
1607    map_channel(lambda do |val|
1608                  sound_interp(intrp,
1609                               (len * 0.5 *
1610                               (1.0 + (read_sample(rd) / mx))).floor)
1611                end)
1612  end
1613
1614  # env_sound_interp takes an envelope that goes between 0 and 1
1615  # (y-axis), and a time-scaler (1.0 = original length) and returns a
1616  # new version of the data in the specified channel that follows that
1617  # envelope (that is, when the envelope is 0 we get sample 0, when
1618  # the envelope is 1 we get the last sample, envelope = 0.5 we get
1619  # the middle sample of the sound and so on. env_sound_interp([0, 0,
1620  # 1, 1]) will return a copy of the current sound;
1621  # env_sound_interp([0, 0, 1, 1, 2, 0], 2.0) will return a new sound
1622  # with the sound copied first in normal order, then reversed.
1623  # src_sound with an envelope could be used for this effect, but it
1624  # is much more direct to apply the envelope to sound sample
1625  # positions.
1626
1627  add_help(:env_sound_interp,
1628           "env_sound_interp(env, time_scale=1.0, snd=false, chn=false)  \
1629Reads SND's channel CHN according to ENV and TIME_SCALE.")
1630  def env_sound_interp(envelope, time_scale = 1.0, snd = false, chn = false)
1631    len = framples(snd, chn)
1632    newlen = (time_scale.to_f * len).floor
1633    read_env = make_env(:envelope, envelope, :length, newlen + 1, :scaler, len)
1634    data = channel2vct(0, false, snd, chn)
1635    new_snd = Vct.new(newlen) do
1636      array_interp(data, env(read_env), len)
1637    end
1638    set_samples(0, newlen, new_snd, snd, chn, true,
1639                format("%s(%p, %s", get_func_name, envelope, time_scale),
1640                0, Current_edit_position, true)
1641  end
1642  # env_sound_interp([0, 0, 1, 1, 2, 0], 2.0)
1643
1644  add_help(:granulated_sound_interp,
1645           "granulated_sound_interp(env, time_scale=1.0, grain_len=0.1, \
1646grain_env=[0, 0, 1, 1, 2, 1, 3, 0], out_hop=0.05, snd=false, chn=false)  \
1647Reads the given channel following ENV (as in env_sound_interp), \
1648using grains to create the re-tempo'd read.")
1649  def granulated_sound_interp(envelope,
1650                              time_scale = 1.0,
1651                              grain_length = 0.1,
1652                              grain_envelope = [0, 0, 1, 1, 2, 1, 3, 0],
1653                              output_hop = 0.05,
1654                              snd = false,
1655                              chn = false)
1656    len = framples(snd, chn)
1657    newlen = (time_scale.to_f * len).floor
1658    read_env = make_env(envelope, :length, newlen, :scaler, len)
1659    sr = srate(snd).to_f
1660    grain_frames = (grain_length * sr).to_i
1661    hop_frames = (output_hop * sr).to_i
1662    num_readers = (grain_length.to_f / output_hop).ceil
1663    cur_readers = 0
1664    next_reader = 0
1665    jitter = sr * 0.005
1666    readers = Array.new(num_readers, false)
1667    grain_envs = Array.new(num_readers) do
1668      make_env(grain_envelope, :length, grain_frames)
1669    end
1670    new_snd = Vct.new(newlen, 0.0)
1671    0.step(newlen, hop_frames) do |i|
1672      stop = [newlen, hop_frames + i].min
1673      read_env.location = i
1674      position_in_original = env(read_env)
1675      mx = [0, (position_in_original + mus_random(jitter)).round].max
1676      readers[next_reader] = make_sampler(mx, snd, chn)
1677      grain_envs[next_reader].reset
1678      next_reader += 1
1679      next_reader %= num_readers
1680      if cur_readers < next_reader
1681        cur_readers = next_reader
1682      end
1683      (0...cur_readers).each do |j|
1684        en = grain_envs[j]
1685        rd = readers[j]
1686        (i...stop).each do |k|
1687          new_snd[k] = env(en) * rd.call()
1688        end
1689      end
1690    end
1691    set_samples(0, newlen, new_snd, snd, chn, true,
1692                format("%s(%p, %s, %s, %p, %s",
1693                       get_func_name,
1694                       envelope,
1695                       time_scale,
1696                       grain_length,
1697                       grain_envelope,
1698                       output_hop), 0, Current_edit_position, true)
1699  end
1700  # granulated_sound_interp([0, 0, 1, 0.1, 2, 1], 1.0, 0.2, [0, 0, 1, 1, 2, 0])
1701  # granulated_sound_interp([0, 0, 1, 1], 2.0)
1702  # granulated_sound_interp([0, 0, 1, 0.1, 2, 1], 1.0, 0.2, [0, 0, 1, 1, 2, 0],
1703  #                         0.02)
1704
1705  # filtered-env
1706
1707  add_help(:filtered_env,
1708           "filtered_env(env, snd=false, chn=false)  \
1709Is a time-varying one-pole filter: when ENV is at 1.0, no filtering, \
1710as ENV moves to 0.0, low-pass gets more intense; \
1711amplitude and low-pass amount move together.")
1712  def filtered_env(en, snd = false, chn = false)
1713    flt = make_one_pole(1.0, 0.0)
1714    amp_env = make_env(:envelope, en, :length, framples())
1715    map_channel(lambda do |val|
1716                  env_val = env(amp_env)
1717                  set_mus_xcoeff(flt, 0, env_val)
1718                  set_mus_ycoeff(flt, 1, env_val - 1.0)
1719                  one_pole(flt, env_val * val)
1720                end, 0, false, snd, chn, false,
1721                format("%s(%s", get_func_name, en.inspect))
1722  end
1723
1724  # lisp graph with draggable x axis
1725
1726  class Mouse
1727    def initialize
1728      @down = 0
1729      @pos = 0.0
1730      @x1 = 1.0
1731    end
1732
1733    def press(snd, chn, button, state, x, y)
1734      @pos = x / @x1
1735      @down = @x1
1736    end
1737
1738    def drag(snd, chn, button, state, x, y)
1739      xnew = x / @x1
1740      @x1 = [1.0, [0.1, @down + (@pos - xnew)].max].min
1741      graph(make_vct!((100 * @x1).floor) do |i| i * 0.01 end, "ramp", 0.0, @x1)
1742    end
1743  end
1744
1745=begin
1746  let(Mouse.new) do |mouse|
1747    $mouse_drag_hook.add_hook!("Mouse") do |snd, chn, button, state, x, y|
1748      mouse.drag(snd, chn, button, state, x, y)
1749    end
1750    $mouse_press_hook.add_hook!("Mouse") do |snd, chn, button, state, x, y|
1751      mouse.press(snd, chn, button, state, x, y)
1752    end
1753  end
1754=end
1755
1756  # pointer focus within Snd
1757  #
1758  # $mouse_enter_graph_hook.add_hook!("focus") do |snd, chn|
1759  #   focus_widget(channel_widgets(snd, chn)[0])
1760  # end
1761  # $mouse_enter_listener_hook.add_hook!("focus") do |widget|
1762  #   focus_widget(widget)
1763  # end
1764  # $mouse_enter_text_hook.add_hook!("focus") do |widget|
1765  #   focus_widget(widget)
1766  # end
1767
1768  # View: Files dialog chooses which sound is displayed
1769  #
1770  # by Anders Vinjar
1771
1772  add_help(:files_popup_buffer,
1773           "files_popup_buffer(type, position, name)  \
1774Hides all sounds but the one the mouse touched in the current files list.  \
1775Use with $mouse_enter_label_hook.
1776$mouse_enter_label_hook.add_hook!(\"files-popup\") do |type, position, name|
1777  files_popup_buffer(type, position, name)
1778end")
1779  def files_popup_buffer(type, position, name)
1780    if snd = find_sound(name)
1781      curr_buffer = Snd.snd
1782      vals = widget_size(sound_widgets(curr_buffer)[0])
1783      height = vals[1]
1784      Snd.sounds.each do |s| hide_widget(sound_widgets(s)[0]) end
1785      show_widget(sound_widgets(snd)[0])
1786      set_widget_size(sound_widgets(snd)[0], [widht, height])
1787      select_sound(snd)
1788    end
1789  end
1790
1791  # remove-clicks
1792
1793  add_help(:find_click,
1794           "find_click(loc)  \
1795Finds the next click starting at LOC.")
1796  def find_click(loc)
1797    reader = make_sampler(loc, false, false)
1798    samp0 = samp1 = samp2 = 0.0
1799    samps = make_vct(10)
1800    len = framples()
1801    samps_ctr = 0
1802    (loc...len).each do |ctr|
1803      samp0, samp1, samp2 = samp1, samp2, next_sample(reader)
1804      samps[samps_ctr] = samp0
1805      if samps_ctr < 9
1806        samps_ctr += 1
1807      else
1808        samps_ctr = 0
1809      end
1810      local_max = [0.1, vct_peak(samps)].max
1811      if ((samp0 - samp1).abs > local_max) and
1812          ((samp1 - samp2).abs > local_max) and
1813          ((samp0 - samp2).abs < (local_max / 2))
1814        return ctr - 1
1815      end
1816    end
1817    false
1818  end
1819
1820  add_help(:remove_clicks,
1821           "remove_clicks()  \
1822Tries to find and smooth-over clicks.")
1823  def remove_clicks
1824    loc = 0
1825    while (click = find_click(loc))
1826      smooth_sound(click - 2, 4)
1827      loc = click + 2
1828    end
1829  end
1830
1831  # searching examples (zero+, next-peak)
1832
1833  add_help(:search_for_click,
1834           "search_for_click()  \
1835Looks for the next click (for use with C-s).")
1836  def search_for_click
1837    samp0 = samp1 = samp2 = 0.0
1838    samps = Vct.new(10)
1839    sctr = 0
1840    lambda do |val|
1841      samp0, samp1, samp2 = samp1, samp2, val
1842      samps[sctr] = val
1843      sctr += 1
1844      if sctr >= 10 then sctr = 0 end
1845      local_max = [0.1, samps.peak].max
1846      if ((samp0 - samp1).abs >= local_max) and
1847          ((samp1 - samp2).abs >= local_max) and
1848          ((samp0 - samp2).abs <= (local_max / 2))
1849        -1
1850      else
1851        false
1852      end
1853    end
1854  end
1855
1856  add_help(:zero_plus,
1857           "zero_plus()  \
1858Finds the next positive-going \
1859zero crossing (if searching forward) (for use with C-s).")
1860  def zero_plus
1861    lastn = 0.0
1862    lambda do |n|
1863      rtn = lastn < 0.0 and n >= 0.0 and -1
1864      lastn = n
1865      rtn
1866    end
1867  end
1868
1869  add_help(:next_peak,
1870           "next_peak()  \
1871Finds the next max or min point \
1872in the time-domain waveform (for use with C-s).")
1873  def next_peak
1874    last0 = last1 = false
1875    lambda do |n|
1876      rtn = number?(last0) and
1877        ((last0 < last1 and last1 > n) or (last0 > last1 and last1 < n)) and -1
1878      last0, last1 = last1, n
1879      rtn
1880    end
1881  end
1882
1883  add_help(:find_pitch,
1884           "find_pitch(pitch)  \
1885Finds the point in the current sound where PITCH (in Hz) \
1886predominates -- C-s find_pitch(300).  \
1887In most cases, \
1888this will be slightly offset from the true beginning of the note.")
1889  def find_pitch(pitch)
1890    interpolated_peak_offset = lambda do |la, ca, ra|
1891      pk = 0.001 + [la, ca, ra].max
1892      logla = log([la, 0.0000001].max / pk) / log(10)
1893      logca = log([ca, 0.0000001].max / pk) / log(10)
1894      logra = log([ra, 0.0000001].max / pk) / log(10)
1895      0.5 * (logla - logra) / ((logla + logra) - 2 * logca)
1896    end
1897    data = make_vct(transform_size)
1898    data_loc = 0
1899    lambda do |n|
1900      data[data_loc] = n
1901      data_loc += 1
1902      rtn = false
1903      if data_loc == transform_size
1904        data_loc = 0
1905        if vct_peak(data) > 0.001
1906          spectr = snd_spectrum(data, Rectangular_window, transform_size)
1907          pk = 0.0
1908          pkloc = 0
1909          (transform_size / 2).times do |i|
1910            if spectr[i] > pk
1911              pk = spectr[i]
1912              pkloc = i
1913            end
1914          end
1915          pit = (pkloc + (pkloc > 0 ?
1916                 interpolated_peak_offset.call(*spectr[pkloc - 1, 3]) :
1917                 0.0) * srate()) / transform_size
1918          if (pitch - pit).abs < srate / (2 * transform_size)
1919            rtn = -(transform_size / 2)
1920          end
1921        end
1922        vct_fill!(data, 0.0)
1923      end
1924      rtn
1925    end
1926  end
1927
1928  # file2vct and a sort of cue-list, I think
1929
1930  add_help(:file2vct,
1931           "file2vct(file)  \
1932Returns a vct with FILE's data.")
1933  def file2vct(file)
1934    len = mus_sound_framples(file)
1935    reader = make_sampler(0, file)
1936    data = make_vct!(len) do next_sample(reader) end
1937    free_sampler(reader)
1938    data
1939  end
1940
1941  add_help(:add_notes,
1942           "add_notes(notes, snd=false, chn=false)  \
1943Adds (mixes) NOTES which is a list of lists of the form: \
1944[file, offset=0.0, amp=1.0] starting at the cursor in the \
1945currently selected channel: \
1946add_notes([[\"oboe.snd\"], [\"pistol.snd\", 1.0, 2.0]])")
1947  def add_notes(notes, snd = false, chn = false)
1948    start = cursor(snd, chn)
1949    as_one_edit_rb("%s(%s", get_func_name, notes.inspect) do
1950      (notes or []).each do |note|
1951        file, offset, amp = note
1952        beg = start + (srate(snd) * (offset or 0.0)).floor
1953        if amp and amp != 1.0
1954          mix_vct(vct_scale!(file2vct(file), amp), beg, snd, chn, false,
1955                  format("%s(%s", get_func_name, notes.inspect))
1956        else
1957          mix(file, beg, 0, snd, chn, false)
1958        end
1959      end
1960    end
1961  end
1962
1963  add_help(:region_play_list,
1964           "region_play_list(data)  \
1965DATA is list of lists [[time, reg], ...], time in secs, \
1966setting up a sort of play list: \
1967region_play_list([[0.0, 0], [0.5, 1], [1.0, 2], [1.0, 0]])")
1968  def region_play_list(data)
1969    (data or []).each do |tm, rg|
1970      tm = (1000.0 * tm).floor
1971      if region?(rg)
1972        call_in(tm, lambda do | | play(rg) end)
1973      end
1974    end
1975  end
1976
1977  add_help(:region_play_sequence,
1978           "region_play_sequence(data)  \
1979DATA is list of region ids which will be played one after the other: \
1980region_play_sequence([0, 2, 1])")
1981  def region_play_sequence(data)
1982    time = 0.0
1983    region_play_list(data.map do |id|
1984                       cur = time
1985                       time = time + region_framples(id) / region_srate(id)
1986                       [cur, id]
1987                     end)
1988  end
1989
1990  # replace-with-selection
1991
1992  add_help(:replace_with_selection,
1993           "replace_with_selection()  \
1994Replaces the samples from the cursor with the current selection.")
1995  def replace_with_selection
1996    beg = cursor
1997    len = selection_framples()
1998    delete_samples(beg, len)
1999    insert_selection(beg)
2000  end
2001
2002  # explode-sf2
2003
2004  add_help(:explode_sf2,
2005           "explode_sf2()  \
2006Turns the currently selected soundfont file into \
2007a bunch of files of the form sample-name.aif.")
2008  def explode_sf2
2009    (soundfont_info() or []).each do |name, start, loop_start, loop_end|
2010      filename = name + ".aif"
2011      if selection?
2012        set_selection_member?(false, true)
2013      end
2014      set_selection_member?(true)
2015      set_selection_position(start)
2016      set_selection_framples(framples() - start)
2017      save_selection(filename, selection_srate(), Mus_bshort, Mus_aifc)
2018      temp = open_sound(filename)
2019      set_sound_loop_info([loop_start, loop_end], temp)
2020      close_sound(temp)
2021    end
2022  end
2023
2024  # open-next-file-in-directory
2025
2026  class Next_file
2027    def initialize
2028      @last_file_opened = ""
2029      @current_directory = ""
2030      @current_sorted_files = []
2031    end
2032
2033    def open_next_file_in_directory
2034      unless $open_hook.member?("open-next-file-in-directory")
2035        $open_hook.add_hook!("open-next-file-in-directory") do |fname|
2036          self.get_current_directory(fname)
2037        end
2038      end
2039      if @last_file_opened.empty? and sounds
2040        @last_file_opened = file_name(Snd.snd)
2041      end
2042      if @current_directory.empty?
2043        unless sounds
2044          get_current_files(Dir.pwd)
2045        else
2046          get_current_files(File.split(@last_file_opened).first)
2047        end
2048      end
2049      if @current_sorted_files.empty?
2050        Snd.raise(:no_such_file)
2051      else
2052        next_file = find_next_file
2053        if find_sound(next_file)
2054          Snd.raise(:file_already_open, next_file)
2055        else
2056          sounds and close_sound(Snd.snd)
2057          open_sound(next_file)
2058        end
2059      end
2060      true
2061    end
2062
2063    private
2064    def find_next_file
2065      choose_next = @last_file_opened.empty?
2066      just_filename = File.basename(@last_file_opened)
2067      f = callcc do |ret|
2068        @current_sorted_files.each do |file|
2069          ret.call(file) if choose_next
2070          if file == just_filename
2071            choose_next = true
2072          end
2073        end
2074        @current_sorted_files[0] # wrapped around
2075      end
2076      @current_directory + "/" + f
2077    end
2078
2079    def get_current_files(dir)
2080      @current_directory = dir
2081      @current_sorted_files = sound_files_in_directory(dir).sort
2082    end
2083
2084    def get_current_directory(filename)
2085      Snd.display(@last_file_opened = filename)
2086      new_path = File.split(mus_expand_filename(filename)).first
2087      if @current_directory.empty? or @current_directory != new_path
2088        get_current_files(new_path)
2089      end
2090      false
2091    end
2092  end
2093
2094  def click_middle_button_to_open_next_file_in_directory
2095    nf = Next_file.new
2096    $mouse_click_hook.add_hook!("next-file") do |s, c, button, st, x, y, ax|
2097      if button == 2
2098        nf.open_next_file_in_directory
2099      end
2100    end
2101  end
2102
2103  # chain-dsps
2104
2105  def chain_dsps(start, dur, *dsps)
2106    dsp_chain = dsps.map do |gen|
2107      if array?(gen)
2108        make_env(:envelope, gen, :duration, dur)
2109      else
2110        gen
2111      end
2112    end
2113    run_instrument(start, dur) do
2114      val = 0.0
2115      dsp_chain.each do |gen|
2116        if env?(gen)
2117          val *= gen.run
2118        elsif readin?(gen)
2119          val += gen.run
2120        else
2121          val = gen.run(val)
2122        end
2123      end
2124      val
2125    end
2126  end
2127
2128=begin
2129  with_sound() do
2130    chain_dsps(0, 1.0, [0, 0, 1, 1, 2, 0],
2131               make_oscil(:frequency, 440))
2132    chain_dsps(0, 1.0, [0, 0, 1, 1, 2, 0],
2133               make_one_pole(0.5), make_readin("oboe.snd"))
2134    chain_dsps(0, 1.0, [0, 0, 1, 1, 2, 0],
2135               let(make_oscil(:frequency, 220),
2136                   make_oscil(:frequency, 440)) do |osc1, osc2|
2137                  lambda do |val|
2138                    osc1.run(val) + osc2.run(2.0 * val)
2139                  end
2140               end)
2141  end
2142=end
2143
2144  # re-order channels
2145
2146  def scramble_channels(*new_order)
2147    len = new_order.length
2148    swap_once = lambda do |current, desired, n|
2149      if n != len
2150        cur_orig, cur_cur = current[n][0, 2]
2151        dst = desired[n]
2152        if cur_orig != dst
2153          swap_channels(false, cur_cur, false, dst)
2154          current[dst][0] = cur_orig
2155        end
2156        swape_once.call(current, desired, n + 1)
2157      end
2158    end
2159    swap_once.call(make_array(len) do |i| [i, i] end, new_order, 0)
2160  end
2161
2162  def scramble_channel(silence)
2163    buffer = make_moving_average(128)
2164    silence = silence / 128.0
2165    edges = []
2166    in_silence = true
2167    old_max = max_regions
2168    old_tags = with_mix_tags
2169    set_max_regions(1024)
2170    set_with_mix_tags(false)
2171    rd = make_sampler()
2172    framples().times do |i|
2173      y = next_sample(rd)
2174      sum_of_squares = moving_average(buffer, y * y)
2175      now_silent = (sum_of_squares < silence)
2176      if now_silent != in_silence
2177        edges.push(i)
2178      end
2179      in_silence = now_silent
2180    end
2181    edges.push(framples())
2182    len = edges.length
2183    start = 0
2184    pieces = Array.new(len) do |i|
2185      en = edges[i]
2186      re = make_region(start, en)
2187      start = en
2188      re
2189    end
2190    start = 0
2191    fnc = lambda do | |
2192      scale_by(0.0)
2193      len.times do |i|
2194        this = random(len)
2195        reg = pieces[this]
2196        pieces[this] = false
2197        unless reg
2198          (this + 1).upto(len - 1) do |j|
2199            reg = pieces[j]
2200            if reg
2201              pieces[j] = false
2202              break
2203            end
2204          end
2205          unless reg
2206            (this - 1).downto(0) do |j|
2207              reg = pieces[j]
2208              if reg
2209                pieces[j] = false
2210                break
2211              end
2212            end
2213          end
2214        end
2215        mix_region(reg, start)
2216        start += framples(reg)
2217        forget_region(reg)
2218      end
2219    end
2220    as_one_edit(fnc)
2221    set_max_regions(old_max)
2222    set_with_mix_tags(old_tags)
2223  end
2224  # scramble_channel(0.01)
2225
2226  # reorder blocks within channel
2227
2228  add_help(:reverse_by_blocks,
2229           "reverse_by_blocks(block_len, snd=false, chn=false)  \
2230Divide sound into block-len blocks, recombine blocks in reverse order.")
2231  def reverse_by_blocks(block_len, snd = false, chn = false)
2232    len = framples(snd, chn)
2233    num_blocks = (len / (srate(snd).to_f * block_len)).floor
2234    if num_blocks > 1
2235      actual_block_len = len / num_blocks
2236      rd = make_sampler(len - actual_block_len, snd, chn)
2237      beg = 0
2238      ctr = 1
2239      map_channel(lambda do |y|
2240                    val = read_sample(rd)
2241                    if beg < 10
2242                      val = val * beg * 0.1
2243                    else
2244                      if beg > actual_block_len - 10
2245                        val = val * (actual_block_len - beg) * 0.1
2246                      end
2247                    end
2248                    beg += 1
2249                    if beg == actual_block_len
2250                      ctr += 1
2251                      beg = 0
2252                      rd = make_sampler([len - ctr * actual_block_len, 0].max,
2253                                        snd, chn)
2254                    end
2255                    val
2256                  end, 0, false,
2257                  snd, chn, false, format("%s(%s", get_func_name, block_len))
2258    end
2259  end
2260
2261  add_help(:reverse_within_blocks,
2262           "reverse_within_blocks(block_len, snd=false, chn=false)  \
2263Divide sound into blocks, recombine in order, \
2264but each block internally reversed.")
2265  def reverse_within_blocks(block_len, snd = false, chn = false)
2266    len = framples(snd, chn)
2267    num_blocks = (len / (srate(snd).to_f * block_len)).floor
2268    if num_blocks > 1
2269      actual_block_len = len / num_blocks
2270      no_clicks_env = [0.0, 0.0, 0.01, 1.0, 0.99, 1.0, 1.0, 0.0]
2271      as_one_edit(lambda do | |
2272                    0.step(len, actual_block_len) do |beg|
2273                      reverse_channel(beg, actual_block_len, snd, chn)
2274                      env_channel(no_clicks_env, beg, actual_block_len,
2275                                  snd, chn)
2276                    end
2277                  end, format("%s(%s", get_func_name, block_len))
2278    else
2279      reverse_channel(0, false, snd, chn)
2280    end
2281  end
2282
2283  def segment_maxamp(name, beg, dur)
2284    mx = 0.0
2285    rd = make_sampler(beg, name)
2286    dur.times do mx = [mx, next_sample(rd).abs].max end
2287    free_sampler(rd)
2288    mx
2289  end
2290
2291  def segment_sound(name, high, low)
2292    len = mus_sound_framples(name)
2293    reader = make_sampler(0, name)
2294    avg = make_moving_average(:size, 128)
2295    lavg = make_moving_average(:size, 2048)
2296    segments = Vct.new(100)
2297    segctr = 0
2298    possible_end = 0
2299    in_sound = false
2300    len.times do |i|
2301      samp = next_sample(reader).abs
2302      val = moving_average(avg, samp)
2303      lval = moving_average(lavg, samp)
2304      if in_sound
2305        if val < low
2306          possible_end = i
2307          if lval < low
2308            segments[segctr] = possible_end + 128
2309            segctr += 1
2310            in_sound = false
2311          end
2312        else
2313          if val > high
2314            segments[segctr] = i - 128
2315            segctr += 1
2316            in_sound = true
2317          end
2318        end
2319      end
2320    end
2321    free_sampler(reader)
2322    if in_sound
2323      segments[segctr] = len
2324      [segctr + 1, segments]
2325    else
2326      [segctr, segments]
2327    end
2328  end
2329
2330  def do_one_directory(fd, dir_name, ins_name, high = 0.01, low = 0.001)
2331    snd_print("# #{dir_name}")
2332    sound_files_in_directory(dir_name).each do |sound|
2333      sound_name = dir_name + "/" + sound
2334      boundary_data = segment_sound(sound_name, high, low)
2335      segments, boundaries = boundary_data
2336      fd.printf("\n\n#    ", sound)
2337      fd.printf("(%s %s", ins_name, sound_name.inspect)
2338      0.step(segments, 2) do |bnd|
2339        segbeg = boundaries[bnd].to_i
2340        segbeg = boundaries[bnd + 1].to_i
2341        fd.printf("(%s %s %s)", segbeg, segdur,
2342                  segment_maxamp(sound_name, segbeg, segdur))
2343        fd.printf(")")
2344      end
2345      mus_sound_forget(sound_name)
2346    end
2347  end
2348
2349  def sound2segment_data(main_dir, output_file = "sounds.data")
2350    File.open(output_file, "w") do |fd|
2351      old_fam = with_file_monitor
2352      set_with_file_monitor(false)
2353      fd.printf("# sound data from %s", main_dir.inspect)
2354      if main_dir[-1] != "/" then main_dir += "/" end
2355      Dir[main_dir].each do |dir|
2356        ins_name = dir.downcase.tr(" ", "")
2357        fd.printf("\n\n# ---------------- %s ----------------", dir)
2358        if dir == "Piano"
2359          Dir[main_dir + dir].each do |inner_dir|
2360            do_one_directory(fd, main_dir + dir + "/" + inner_dir,
2361                             ins_name, 0.001, 0.0001)
2362          end
2363        else
2364          do_one_directory(fd, main_dir + dir, ins_name)
2365        end
2366      end
2367      set_with_file_monitor(old_fam)
2368    end
2369  end
2370  # sounds2segment_data(ENV['HOME'] + "/.snd.d/iowa/sounds/", "iowa.data")
2371
2372  add_help(:channel_clipped?,
2373           "channel_clipped?(snd=false, chn=false)  \
2374Returns true and a sample number if it finds clipping.")
2375  def channel_clipped?(snd = false, chn = false)
2376    last_y = 0.0
2377    scan_channel(lambda do |y|
2378                   result = (y.abs >= 0.9999 and last_y.abs >= 0.9999)
2379                   last_y = y
2380                   result
2381                 end, 0, false, snd, chn)
2382  end
2383
2384  # scan-sound
2385
2386  def scan_sound(func, beg = 0, dur = false, snd = false)
2387    if sound?(index = Snd.snd(snd))
2388      if (chns = channels(index)) == 1
2389        scan_channel(lambda do |y| func.call(y, 0) end, beg, dur, index, 0)
2390      else
2391        len = framples(index)
2392        fin = (dur ? [len, beg + dur].min : len)
2393        readers = make_array(chns) do |chn| make_sampler(beg, index, chn) end
2394        result = false
2395        beg.upto(fin) do |i|
2396          local_result = true
2397          readers.each_with_index do |rd, chn|
2398            local_result = (func.call(rd.call, chn) and local_result)
2399          end
2400          if local_result
2401            result = [true, i]
2402            break
2403          end
2404        end
2405        result
2406      end
2407    else
2408      Snd.raise(:no_such_sound, get_func_name, snd)
2409    end
2410  end
2411
2412  def scan_sound_rb(beg = 0, dur = false, snd = false, &func)
2413    scan_sound(func, beg, dur, snd)
2414  end
2415end
2416
2417include Examp
2418
2419module Moog
2420  class Moog_filter < Musgen
2421    Gaintable = vct(0.999969, 0.990082, 0.980347, 0.970764, 0.961304, 0.951996,
2422                    0.94281, 0.933777, 0.924866, 0.916077, 0.90741, 0.898865,
2423                    0.890442, 0.882141, 0.873962, 0.865906, 0.857941, 0.850067,
2424                    0.842346, 0.834686, 0.827148, 0.819733, 0.812378, 0.805145,
2425                    0.798004, 0.790955, 0.783997, 0.77713, 0.770355, 0.763672,
2426                    0.75708, 0.75058, 0.744141, 0.737793, 0.731537, 0.725342,
2427                    0.719238, 0.713196, 0.707245, 0.701355, 0.695557, 0.689819,
2428                    0.684174, 0.678558, 0.673035, 0.667572, 0.66217, 0.65686,
2429                    0.651581, 0.646393, 0.641235, 0.636169, 0.631134, 0.62619,
2430                    0.621277, 0.616425, 0.611633, 0.606903, 0.602234, 0.597626,
2431                    0.593048, 0.588531, 0.584045, 0.579651, 0.575287, 0.570953,
2432                    0.566681, 0.562469, 0.558289, 0.554169, 0.550079, 0.546051,
2433                    0.542053, 0.538116, 0.53421, 0.530334, 0.52652, 0.522736,
2434                    0.518982, 0.515289, 0.511627, 0.507996, 0.504425, 0.500885,
2435                    0.497375, 0.493896, 0.490448, 0.487061, 0.483704, 0.480377,
2436                    0.477081, 0.473816, 0.470581, 0.467377, 0.464203, 0.46109,
2437                    0.457977, 0.454926, 0.451874, 0.448883, 0.445892, 0.442932,
2438                    0.440033, 0.437134, 0.434265, 0.431427, 0.428619, 0.425842,
2439                    0.423096, 0.42038, 0.417664, 0.415009, 0.412354, 0.409729,
2440                    0.407135, 0.404572, 0.402008, 0.399506, 0.397003, 0.394501,
2441                    0.392059, 0.389618, 0.387207, 0.384827, 0.382477, 0.380127,
2442                    0.377808, 0.375488, 0.37323, 0.370972, 0.368713, 0.366516,
2443                    0.364319, 0.362122, 0.359985, 0.357849, 0.355713, 0.353607,
2444                    0.351532, 0.349457, 0.347412, 0.345398, 0.343384, 0.34137,
2445                    0.339417, 0.337463, 0.33551, 0.333588, 0.331665, 0.329773,
2446                    0.327911, 0.32605, 0.324188, 0.322357, 0.320557, 0.318756,
2447                    0.316986, 0.315216, 0.313446, 0.311707, 0.309998, 0.308289,
2448                    0.30658, 0.304901, 0.303223, 0.301575, 0.299927, 0.298309,
2449                    0.296692, 0.295074, 0.293488, 0.291931, 0.290375, 0.288818,
2450                    0.287262, 0.285736, 0.284241, 0.282715, 0.28125, 0.279755,
2451                    0.27829, 0.276825, 0.275391, 0.273956, 0.272552, 0.271118,
2452                    0.269745, 0.268341, 0.266968, 0.265594, 0.264252, 0.262909,
2453                    0.261566, 0.260223, 0.258911, 0.257599, 0.256317, 0.255035,
2454                    0.25375)
2455    Freqtable = [0, -1,
2456      0.03311111, -0.9,
2457      0.06457143, -0.8,
2458      0.0960272, -0.7,
2459      0.127483, -0.6,
2460      0.1605941, -0.5,
2461      0.1920544, -0.4,
2462      0.22682086, -0.3,
2463      0.2615873, -0.2,
2464      0.29801363, -0.1,
2465      0.33278003, -0.0,
2466      0.37086168, 0.1,
2467      0.40893877, 0.2,
2468      0.4536417, 0.3,
2469      0.5, 0.4,
2470      0.5463583, 0.5,
2471      0.5943719, 0.6,
2472      0.6556281, 0.7,
2473      0.72185487, 0.8,
2474      0.8096009, 0.9,
2475      0.87913835, 0.95,
2476      0.9933787, 1,
2477      1, 1]
2478
2479    def initialize(freq, q)
2480      super()
2481      @frequency = freq
2482      @Q = q
2483      @state = make_vct(4)
2484      @A = 0.0
2485      @freqtable = envelope_interp(freq / (srate() * 0.5), Freqtable)
2486    end
2487    attr_reader :frequency, :state, :freqtable, :A
2488    attr_accessor :Q
2489
2490    def inspect
2491      format("%s.new(%s, %s)", self.class, @frequency, @Q)
2492    end
2493
2494    def to_s
2495      format("#<%s freq: %1.3f, Q: %s>", self.class, @frequency, @Q)
2496    end
2497
2498    def run_func(val1 = 0.0, val2 = 0.0)
2499      filter(val1)
2500    end
2501
2502    def frequency=(freq)
2503      @freqtable = envelope_interp(freq / (srate() * 0.5), Freqtable)
2504      @frequency = freq
2505    end
2506
2507    def filter(insig)
2508      a = 0.25 * (insig - @A)
2509      @state.map! do |st|
2510        new_a = saturate(a + @freqtable * (a - st))
2511        a = saturate(new_a + st)
2512        new_a
2513      end
2514      ix = @freqtable * 99.0
2515      ixint = ix.floor
2516      ixfrac = ix - ixint
2517      @A = a * @Q *
2518           ((1.0 - ixfrac) * Gaintable[ixint + 99] +
2519            ixfrac * Gaintable[ixint + 100])
2520      a
2521    end
2522
2523    private
2524    def saturate(x)
2525      [[x, -0.95].max, 0.95].min
2526    end
2527  end
2528
2529  add_help(:make_moog_filter,
2530           "make_moog_filter(freq=440.0, Q=0)  \
2531Makes a new moog_filter generator. \
2532FREQUENCY is the cutoff in Hz, \
2533Q sets the resonance: 0 = no resonance, 1: oscillates at FREQUENCY.")
2534  def make_moog_filter(freq = 440.0, q = 0)
2535    Moog_filter.new(freq, q)
2536  end
2537
2538  add_help(:moog_filter,
2539           "moog_filter(moog, insig=0.0)  \
2540Is the generator associated with make_moog_filter.")
2541  def moog_filter(mg, insig = 0.0)
2542    mg.filter(insig)
2543  end
2544
2545  def moog(freq, q)
2546    mg = Moog_filter.new(freq, q)
2547    lambda do |inval| mg.filter(inval) end
2548  end
2549end
2550
2551include Moog
2552
2553# examp.rb ends here
2554