1# dlocsig.rb -- CLM -> Snd/Ruby translation of dlocsig.lisp
2
3# Translator/Author: Michael Scholz <mi-scholz@users.sourceforge.net>
4# Copyright (c) 2003-2020 Michael Scholz <mi-scholz@users.sourceforge.net>
5# All rights reserved.
6#
7# Redistribution and use in source and binary forms, with or without
8# modification, are permitted provided that the following conditions
9# are met:
10# 1. Redistributions of source code must retain the above copyright
11#    notice, this list of conditions and the following disclaimer.
12# 2. Redistributions in binary form must reproduce the above copyright
13#    notice, this list of conditions and the following disclaimer in the
14#    documentation and/or other materials provided with the distribution.
15#
16# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
17# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
20# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
22# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
23# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
24# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
25# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
26# SUCH DAMAGE.
27
28# Original Copyright of Fernando Lopez Lezcano:
29
30# ;;; Copyright (c) 92, 93, 94, 98, 99, 2000, 2001 Fernando Lopez Lezcano.
31# ;;; All rights reserved.
32# ;;; Use and copying of this software and preparation of derivative works
33# ;;; based upon this software are permitted and may be copied as long as
34# ;;; no fees or compensation are charged for use, copying, or accessing
35# ;;; this software and all copies of this software include this copyright
36# ;;; notice. Suggestions, comments and bug reports are welcome. Please
37# ;;; address email to: nando@ccrma.stanford.edu
38# ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
39
40# ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
41# ;;; Dynamic multichannel three-dimentional signal locator
42# ;;; (wow that sound good! :-)
43# ;;;
44# ;;; by Fernando Lopez Lezcano
45# ;;;    CCRMA, Stanford University
46# ;;;    nando@ccrma.stanford.edu
47# ;;;
48# ;;; Thanks to Juan Pampin for help in the initial coding of the new version
49# ;;; and for prodding me to finish it. To Joseph L. Anderson and Marcelo Perticone
50# ;;; for insights into the Ambisonics coding and decoding process.
51# ;;; http://www.york.ac.uk/inst/mustech/3d_audio/ambison.htm for more details...
52
53# Tested with Snd 7.10, Motif 2.2.2, Ruby 1.6.6, 1.6.8 and 1.9.0.
54#
55# The code is a translation of the Lisp code of Fernando Lopez Lezcano
56# found in clm-2/dlocsig of the CLM distribution.  An extensive
57# documentation of the purpose and usage of it can be found in
58# clm-2/dlocsig/dlocsig.html.
59#
60# Note: dlocsig.rb handles not more rev_channels than out_channels;
61# B_format_ambisonics handles only 4 out_channels and 0, 1, or 4
62# rev_channels.
63#
64# The simple example
65#
66# [[-10, 10], [0, 5], [10, 10]].to_path.snd_plot
67#
68# draws trajectory, velocity, doppler curve, and the acceleration in
69# Snd's lisp-graph.  If you have gnuplot installed, the example
70#
71# [[-10, 10], [0, 5], [10, 10]].to_path.pplot
72#
73# draws all four curves in one gnuplot window.
74
75#   DL.make_path
76#   DL.make_polar_path
77#   DL.make_closed_path
78#   and to_path take the following options
79#
80#   Open_bezier_path.new(path, *args)
81#                        :d3,                  true
82#                        :polar,               false
83#                        :error,               0.01
84#                        :curvature,           nil
85#                        :initial_direction,   [0.0, 0.0, 0.0]
86#                        :final_direction,     [0.0, 0.0, 0.0]
87#
88# Closed_bezier_path.new(path, *args)
89#                        :d3,                  true
90#                        :polar,               false
91#                        :error,               0.01
92#                        :curvature,           nil
93#
94#   DL.make_literal_path
95#   DL.make_literal_polar_path take the following options
96#
97#       Literal_path.new(path, *args)
98#                        :d3,                  true
99#                        :polar,               false
100#
101#   DL.make_spiral_path takes these options
102#
103#        Spiral_path.new :start_angle,         0
104#                        :turns,               2
105#
106# The make_locsig-replacement make_dlocsig takes these arguments:
107#
108#         DL.make_dlocsig(startime, dur, *args)
109#                        :path,                nil
110#                        :scaler,              1.0
111#                        :reverb_amount,       0.05
112#                        :rbm_output,          $output
113#                        :rbm_reverb,          $reverb
114#                        :output_power,        1.5
115#                        :reverb_power,        0.5
116#                        :render_using,        :amplitude_panning
117#                                           or :b_format_ambisonics
118#                                           or :decoded_ambisonics
119#
120# Sample instruments (sinewave() and move() below) show how to replace
121# the usual make_locsig() and locsig() by DL.make_dlocsig() and
122# DL.dlocsig().
123
124# Example functions at the end of the file:
125
126# class Instrument
127#   sinewave(start, dur, freq, amp, path, amp_env, *dlocsig_args)
128#   move(start, file, path, *dlocsig_args)
129#   move_sound(path, *dlocsig_args) do ... end
130#
131# class With_sound
132#   run_dlocsig(start, dur, *dlocsig_args) do |samp| ... end
133
134# Classes and Modules:
135
136# module Inject
137#   inject(n)
138#   sum(initial)
139#   product(initial)
140#
141# class Array
142#   to_trias
143#   to_path(*args)
144#
145# class Sndplot
146#   initialize(chns)
147#   snd
148#   open
149#   close
150#
151# class Gnuplot
152#   initialize
153#   open
154#   close
155#   command(*args)
156#   reset
157#   set_autoscale
158#   set_x_range(range)
159#   set_y_range(range)
160#   set_z_range(range)
161#   set_grid
162#   set_surface
163#   set_parametric
164#   set_ticslevel(level)
165#   set_title(title)
166#   set_label(label)
167#   set_margins(margin)
168#   set_border(border)
169#   start_multiplot
170#   end_multiplot
171#   size(xorigin, yorigin, xsize, ysize)
172#   data(data, *args)
173#   plot_2d_curve(curve, *args)
174#   plot_2d_curves(curves, *args)
175#   plot_3d_curve(curve, *args)
176#
177# module DL
178#   class Dlocsig < Dlocs
179#     initialize(start, dur, *args)
180#     one_turn
181#     one_turn=(val)
182#     speed_of_sound
183#     speed_of_sound=(val)
184#     run_beg
185#     run_end
186#     angles_in_degree
187#     angles_in_radians
188#     angles_in_turns
189#     distance_in_meters
190#     distance_in_feet
191#
192#   class Path
193#     initialize(path, *args)
194#     path_x
195#     path_y
196#     path_z
197#     path_time
198#     scale_path(scaling)
199#     translate_path(translation)
200#     rotate_path(rotation, *args)
201#
202#     path_trajectory
203#     path_2d_trajectory
204#     path_velocity
205#     path_doppler
206#     path_acceleration
207#
208#     plot_open
209#     plot_close
210#     cmd(*args)
211#     plot_trajectory(*args)
212#     plot_velocity(reset)
213#     plot_doppler(reset)
214#     plot_acceleration(reset)
215#     pplot(normalize)
216#
217#     snd_open(chns)
218#     snd_close
219#     snd_trajectory(chn, label)
220#     snd_velocity(chn, label)
221#     snd_doppler(chn, label)
222#     snd_acceleration(chn, label)
223#     snd_plot
224#
225#   DL.make_dlocsig(start, dur, *args)
226#   DL.dlocsig(dl, loc, input)
227#
228#   DL.make_path(path, *args)
229#   DL.make_polar_path(path, *args)
230#   DL.make_closed_path(path, *args)
231#   DL.make_literal_path(path, *args)
232#   DL.make_literal_polar_path(path, *args)
233#   DL.make_spiral_path(*args)
234#
235# class Dlocsig_menu
236#   initialize(label, snd_p)
237#   post_dialog
238
239require "ws"
240require "matrix"
241include Math
242
243provided?(:snd_motif) and (not provided?(:xm)) and require("libxm.so")
244
245class DlocsigError < StandardError
246end
247
248Ruby_exceptions[:dlocsig_error] = DlocsigError
249
250def dl_error(*msg)
251  Snd.raise(:dlocsig_error, (msg.empty? ? "" : format(*msg)))
252end
253
254# module Inject, see Thomas, David, Hunt, Andrew: Programming Ruby --
255# The Pragmatic Programmer's Guide, 2001 Addison-Wesley, page 102n
256
257module Inject
258  def inject(n)
259    each do |x| n = yield(n, x) end
260    n
261  end
262
263  def sum(initial = 0)
264    inject(initial) do |n, v| n + v end
265  end
266
267  def product(initial = 1)
268    inject(initial) do |n, v| n * v end
269  end
270end unless defined? Inject
271
272# used by plotting curves
273# to_trias: [0, 1, 2, 3, 4, 5] --> [[0, 1, 2], [3, 4, 5]]
274# to_path:  [[-10, 10], [0, 5], [10, 10]].to_path <=> DL.make_path([[-10, 10], [0, 5], [10, 10]])
275# uses the same options as DL.make_path()
276
277class Array
278  include Inject
279
280  def to_trias
281    ary = []
282    unless self.length.divmod(3).last.nonzero?
283      0.step(self.length - 2, 3) do |i|
284        ary.push([self[i], self[i + 1], self[i + 2]])
285      end
286    end
287    ary
288  end
289
290  def to_path(*args)
291    DL.make_path(self, *args)
292  end
293end
294
295class Sndplot
296  def initialize(chns = 1)
297    @chns = chns
298    @snd = open
299  end
300  attr_reader :snd
301
302  def inspect
303    format("#<%s: snd: %d, chns: %d>", self.class, @snd, @chns)
304  end
305
306  def open
307    if snds = sounds()
308      snds.each do |s| set_sound_property(:selected, false, s) end
309      set_sound_property(:selected, true, selected_sound)
310    end
311    if @snd = snds.detect do |s| channels(s) >= @chns end
312      set_sound_property(:dlocsig_created, false, @snd)
313      select_sound(@snd)
314    else
315      @snd = new_sound(snd_tempnam, @chns, default_output_srate,
316                       default_output_sample_type,
317                       default_output_header_type)
318      set_sound_property(:dlocsig_created, true, @snd)
319    end
320    channels(@snd).times do |chn|
321      set_channel_property(:time_graph, time_graph?(@snd, chn), @snd, chn)
322      set_channel_property(:transform_graph, transform_graph?(@snd, chn), @snd, chn)
323      set_channel_property(:lisp_graph, lisp_graph?(@snd, chn), @snd, chn)
324      set_time_graph?(false, @snd, chn)
325      set_transform_graph?(false, @snd, chn)
326      set_lisp_graph?(true, @snd, chn)
327    end
328    $exit_hook.add_hook!("dlocsig-hook") do | |
329      close
330      false
331    end
332    @snd
333  end
334
335  def close
336    if snds = sounds()
337      snds.each do |snd|
338        set_sound_property(:selected, false, snd)
339        unless sound_property(:dlocsig_created, snd).nil?
340          if sound_property(:dlocsig_created, snd)
341            close_sound_extend(snd)
342          else
343            channels(snd).times do |chn|
344              set_time_graph?(channel_property(:time_graph, snd, chn), snd, chn)
345              set_transform_graph?(channel_property(:transform_graph, snd, chn), snd, chn)
346              set_lisp_graph?(channel_property(:lisp_graph, snd, chn), snd, chn)
347            end
348          end
349        end
350      end
351    end
352    $exit_hook.remove_hook!("dlocsig-hook")
353    self
354  end
355end
356
357class Gnuplot
358  @@plot_stream = nil
359
360  def initialize
361    if (not @@plot_stream) or @@plot_stream.closed?
362      open
363    end
364  end
365
366  def inspect
367    format("#<%s: plot_stream: %s>", self.class, @@plot_stream.inspect)
368  end
369
370  def open
371    if (gnuplot = `which gnuplot`).empty?
372      dl_error("gnuplot not found?")
373    else
374      @@plot_stream = IO.popen(gnuplot, "w")
375    end
376  end
377
378  def close
379    unless @@plot_stream.closed?
380      @@plot_stream.puts("quit")
381      @@plot_stream.close
382      @@plot_stream = nil
383    end
384  end
385
386  def command(*args)
387    open if @@plot_stream.closed?
388    @@plot_stream.printf(*args)
389    format(*args).chomp
390  rescue
391    Snd.warning("%s#%s", self.class, get_func_name)
392  end
393
394  def reset
395    command "reset\n"
396  end
397
398  def set_autoscale
399    command "set autoscale\n"
400  end
401
402  def set_x_range(range = [])
403    command("set xrange [%f:%f]\n", range[0], range[1]) if range.length == 2
404  end
405
406  def set_y_range(range = [])
407    command("set yrange [%f:%f]\n", range[0], range[1]) if range.length == 2
408  end
409
410  def set_z_range(range = [])
411    command("set zrange [%f:%f]\n", range[0], range[1]) if range.length == 2
412  end
413
414  def set_grid
415    command "set grid xtics; set grid ytics; set grid ztics\n"
416  end
417
418  def set_surface
419    command "set surface\n"
420  end
421
422  def set_parametric
423    command "set parametric\n"
424  end
425
426  def set_ticslevel(level = 0)
427    command("set ticslevel %.2f\n", level)
428  end
429
430  def set_title(title = "")
431    command("set title \"%s\"\n", title) unless title.empty?
432  end
433
434  def set_label(label = "")
435    command("set label \"%s\"\n", label) unless label.empty?
436  end
437
438  def set_margins(margin = 1)
439    command("set tmargin %f\n", margin)
440    command("set lmargin %f\n", margin)
441    command("set rmargin %f\n", margin)
442    command("set bmargin %f\n", margin)
443  end
444
445  def set_border(border = nil)
446    command("set border %d\n", border.to_i) if border
447  end
448
449  def start_multiplot
450    command "set multiplot\n"
451  end
452
453  def end_multiplot
454    command "set nomultiplot\n"
455  end
456
457  def size(xorigin, yorigin, xsize, ysize)
458    command("set origin %f,%f\n", xorigin.to_f, yorigin.to_f)
459    command("set size %f,%f\n", xsize.to_f, ysize.to_f)
460  end
461
462  def data(data, *args)
463    style, label = nil
464    optkey(args, binding,
465           [:style, "linespoints"],
466           [:label, ""])
467    command("plot '-' %s %s\n", label.empty? ? "" : "title \"#{label}\"",
468                                             style.empty? ? "" : "with #{style}")
469    data.each_with_index do |y, x| command("%f %f\n", x, y) end
470    command "e\n"
471  end
472
473  def plot_2d_curve(curve, *args)
474    style, label = nil
475    optkey(args, binding,
476           [:style, "linespoints"],
477           [:label, ""])
478    set_grid()
479    command("plot '-' %s %s\n", label.empty? ? "" : "title \"#{label}\"",
480                                             style.empty? ? "" : "with #{style}")
481    curve.each_pair do |x, y| command("%.8f %.8f\n", x, y) end
482    command "e\n"
483  end
484
485  def plot_2d_curves(curves, *args)
486    styles, labels = nil
487    optkey(args, binding,
488           [:styles, "linespoints"],
489           [:labels, ""])
490    set_grid()
491    styles = curves.map do |i| styles end unless array?(styles)
492    labels = curves.map do |i| labels end unless array?(labels)
493    command "plot"
494    curves.each_with_index do |x, i|
495      style = styles[i]
496      label = labels[i]
497      command " '-' "
498      command(" title \"%s\"", label) if label or (not label.empty?)
499      command(" with %s", style) if style or (not style.empty?)
500      command(", ") if i != (curves.length - 1)
501    end
502    command "\n"
503    curves.each do |curve|
504      curve.each_pair do |x, y| command("%.8f %.8f\n", x, y) end
505      command "e\n"
506    end
507  end
508
509  def plot_3d_curve(curve, *args)
510    style, label, zstyle, xrot, zrot, scale, zscale = nil
511    optkey(args, binding,
512           [:style, "linespoints"],
513           [:label, ""],
514           [:zstyle, "impulses"],
515           :xrot,
516           :zrot,
517           :scale,
518           :zscale)
519    set_border(127 + 256 + 512)
520    set_grid()
521    set_surface()
522    set_parametric()
523    set_ticslevel(0)
524    if xrot or zrot or scale or zscale
525      command("set view %s,%s,%s,%s\n", xrot, zrot, scale, zscale)
526    end
527    command "splot '-'"
528    command(" title \"%s\"", label) unless label.empty?
529    command(" with %s 1", style) unless style.empty?
530    command(", '-' notitle with %s 1", zstyle) unless zstyle.empty?
531    command "\n"
532    curve.to_trias.each do |x, y, z| command("%.8f %.8f %.8f\n", x, y, z) end
533    command "e\n"
534    if zstyle
535      curve.to_trias.each do |x, y, z| command("%.8f %.8f %.8f\n", x, y, z) end
536      command "e\n"
537    end
538  end
539end
540
541module DL
542  Path_maxcoeff = 8
543  Point707 = cos(TWO_PI / 8.0)
544
545  Amplitude_panning   = 1
546  B_format_ambisonics = 2
547  Decoded_ambisonics  = 3
548  def which_render(val)
549    case val
550    when :amplitude_panning, Amplitude_panning
551      Amplitude_panning
552    when :b_format_ambisonics, B_format_ambisonics
553      B_format_ambisonics
554    when :decoded_ambisonics, Decoded_ambisonics
555      Decoded_ambisonics
556    else
557      Amplitude_panning
558    end
559  end
560
561  def cis(r)
562    Complex(cos(r), sin(r))
563  end
564
565  def distance(x, y, z)
566    sqrt(x * x + y * y + z * z)
567  end
568
569  def nearest_point(x0, y0, z0, x1, y1, z1, px, py, pz)
570    if same?(x0, y0, z0, px, py, pz)
571      [x0, y0, z0]
572    elsif same?(x1, y1, z1, px, py, pz)
573      [x1, y1, z1]
574    elsif same?(x0, y0, z0, x1, y1, z1)
575      [x0, y0, z0]
576    else
577      xm0 = x1 - x0
578      ym0 = y1 - y0
579      zm0 = z1 - z0
580      xm1 = px - x0
581      ym1 = py - y0
582      zm1 = pz - z0
583      d0 = distance(xm0, ym0, zm0)
584      d1 = distance(xm1, ym1, zm1)
585      p = d1 * ((xm0 * xm1 + ym0 * ym1 + zm0 * zm1) / (d0 * d1))
586      ratio = p / d0
587      [x0 + xm0 * ratio, y0 + ym0 * ratio, z0 + zm0 * ratio]
588    end
589  end
590
591  def same?(a0, b0, c0, a1, b1, c1)
592    a0 == a1 and b0 == b1 and c0 == c1
593  end
594
595  def rotation_matrix(x, y, z, angle)
596    mag = distance(x, y, z)
597    dx = x / mag
598    dy = y / mag
599    dz = z / mag
600    ri = Matrix.I(3)
601    ra = Matrix.rows([[0.0, dz, -dy], [-dz, 0.0, dx], [dy, -dx, 0.0]])
602    raa = ra * ra
603    sn = sin(-angle)
604    omcs = 1 - cos(-angle)
605    raa = raa.map do |xx| omcs * xx end
606    ra = ra.map do |xx| sn * xx end
607    (ri + ra + raa)
608  end
609
610  class Dlocsig_base
611    include DL
612
613    def initialize
614      @one_turn = 360.0
615      @speed_of_sound = 344.0
616    end
617    attr_accessor :one_turn, :speed_of_sound
618
619    def angles_in_degree
620      @one_turn = 360.0
621    end
622
623    def angles_in_radians
624      @one_turn = TWO_PI
625    end
626
627    def angles_in_turns
628      @one_turn = 1.0
629    end
630
631    def distances_in_meters
632      @speed_of_sound = 344.0
633    end
634
635    def distances_in_feet
636      @speed_of_sound = 1128.0
637    end
638  end
639
640  class Speaker_config < Dlocsig_base
641    Groups = Struct.new("Groups", :size, :vertices, :speakers, :matrix)
642
643    def initialize
644      super
645      @number = nil
646      @coords = nil
647      @groups = nil
648    end
649
650    protected
651    def set_speakers(channels, d3)
652      if channels.between?(1, 8)
653        d3 = false if channels < 4
654        arrange_speakers(unless d3
655                           case channels
656                           when 1
657                             [[0]]
658                           when 2
659                             [[-60, 60]]
660                           when 3
661                             [[-45, 45, 180]]
662                           when 4
663                             [[-45, 45, 135, 225]]
664                           when 5
665                             [[-45, 0, 45, 135, -135]]
666                           when 6
667                             [[-60, 0, 60, 120, 180, 240]]
668                           when 7
669                             [[-45, 0, 45, 100, 140, -140, -100]]
670                           when 8
671                             [[-22.5, 22.5, 67.5, 112.5, 157.5, 202.5, 247.5, 292.5]]
672                           end
673                         else
674                           case channels
675                           when 4
676                             [[[-60, 0], [60, 0], [180, 0], [0, 90]],
677                              [[0, 1, 3], [1, 2, 3], [2, 0, 3], [0, 1, 2]]]
678                           when 5
679                             [[[-45, 0], [45, 0], [135, 0], [-135, 0], [0, 90]],
680                              [[0, 1, 4], [1, 2, 4], [2, 3, 4], [3, 0, 4], [0, 1, 2], [2, 3, 0]]]
681                           when 6
682                             [[[-45, 0], [45, 0], [135, 0], [-135, 0], [-90, 60], [90, 60]],
683                              [[0, 1, 4], [1, 4, 5], [1, 2, 5], [2, 3, 5],
684                               [3, 4, 5], [3, 0, 4], [0, 1, 2], [2, 3, 0]]]
685                           when 7
686                             [[[-45, 0], [45, 0], [135, 0], [-135, 0],
687                               [-60, 60], [60, 60], [180, 60]],
688                              [[0, 1, 4], [1, 4, 5], [1, 2, 5], [2, 6, 5], [2, 3, 6],
689                               [3, 4, 6], [3, 0, 4], [4, 5, 6], [0, 1, 2], [2, 3, 0]]]
690                           when 8
691                             [[[-45, 10], [45, -10], [135, -10], [225, -10],
692                               [-45, 45], [45, 45], [135, 45], [225, 45]],
693                              [[0, 4, 5], [0, 5, 1], [5, 1, 2], [2, 6, 5], [6, 7, 2], [2, 3, 7],
694                               [3, 7, 4], [3, 0, 4], [4, 7, 6], [6, 5, 4], [0, 1, 2], [2, 3, 0]]]
695                           end
696                         end)
697      else
698        dl_error(channels, "only 1 to 8 channels possible")
699      end
700    end
701
702    private
703    def arrange_speakers(args)
704      speakers = args.shift
705      groups = args.shift
706      @number = speakers.length
707      @coords = speakers.map do |s|
708        a = (array?(s) ? s[0] : s).to_f
709        e = (array?(s) ? s[1] : 0).to_f
710        evec = cis((e / @one_turn) * TWO_PI)
711        dxy = evec.real
712        avec = cis((a / @one_turn) * TWO_PI)
713        x = (dxy * avec.imag)
714        y = (dxy * avec.real)
715        z = evec.imag
716        mag = distance(x, y, z)
717        [x / mag, y / mag, z / mag]
718      end
719      unless groups
720        if @number == 1
721          groups = [[0]]
722        else
723          groups = make_array(@number) do |i| [i, i + 1] end
724          groups[-1][-1] = 0
725        end
726      end
727      @groups = groups.map do |group|
728        size = group.length
729        vertices = group.map do |vertice| @coords[vertice] end
730        matrix = case size
731                 when 3
732                   if (m = Matrix[vertices[0], vertices[1], vertices[2]]).regular?
733                     m.inverse.to_a
734                   else
735                     nil
736                   end
737                 when 2
738                   if (m = Matrix[vertices[0][0, 2], vertices[1][0, 2]]).regular?
739                     m.inverse.to_a
740                   else
741                     nil
742                   end
743                 else
744                   nil
745                 end
746        Groups.new(size, vertices, group, matrix)
747      end
748    end
749  end
750
751  class Dlocsig < Speaker_config
752    def initialize
753      super
754      @render_using = Amplitude_panning
755      @output_power = 1.5
756      @reverb_power = 0.5
757      @rbm_output = nil
758      @rbm_reverb = nil
759      @out_channels = 4
760      @rev_channels = 1
761      @clm = true
762      @delay = []
763      @prev_time = @prev_dist = @prev_group = @prev_x = @prev_y = @prev_z = false
764      @first_dist = @last_dist = 0.0
765      @min_dist = @max_dist = 0.0
766      @start = nil
767      @end = nil
768      @output_gains = nil
769      @reverb_gains = nil
770      @path = nil
771      @run_beg = @run_end = nil
772    end
773    attr_reader :run_beg, :run_end, :out_channels, :rev_channels
774
775    def inspect
776      format("#<%s: channels: %d, reverb: %s>", self.class, @out_channels, @rev_channels.inspect)
777    end
778
779    def each
780      (@run_beg...@run_end).each do |i| yield(i) end
781    end
782    alias run each
783
784    # general clm version
785    # dl.dlocsig(loc, val)
786    def dlocsig(loc, input)
787      if loc < @start
788        delay(@path, (loc >= @end ? 0.0 : input), 0.0)
789        @out_channels.times do |chn| out_any(loc, 0.0, chn, @rbm_output) end
790      else
791        sample = delay(@path, (loc >= @end ? 0.0 : input), env(@delays))
792        @out_channels.times do |chn|
793          out_any(loc, sample * env(@output_gains[chn]), chn, @rbm_output)
794        end
795        @rev_channels.times do |chn|
796          out_any(loc, sample * env(@reverb_gains[chn]), chn, @rbm_reverb)
797        end
798      end
799    end
800
801    # dl.ws_dlocsig do |loc| ...; val; end
802    # @clm == true    @rbm_output/@rbm_reverb: sample2files
803    # @clm == false   @rbm_output/@rbm_reverb: sound index numbers
804    # With_sound#run_dlocsig below uses this method
805    def ws_dlocsig
806      len = @run_end - @run_beg
807      out_data = make_vct(len)
808      len.times do |i|
809        loc = i + @run_beg
810        input = yield(loc)
811        if loc < @start
812          delay(@path, (loc >= @end ? 0.0 : input), 0.0)
813        else
814          out_data[i] = delay(@path, (loc >= @end ? 0.0 : input), env(@delays))
815        end
816      end
817      @out_channels.times do |chn|
818        if @clm
819          (@run_beg...@run_end).each do |i|
820            out_any(i, out_data[i - @run_beg] * env(@output_gains[chn]), chn, @rbm_output)
821          end
822        else
823          out = vct_multiply!(vct_copy(out_data),
824                              Vct.new(len) do |x| env(@output_gains[chn]) end)
825          mix_vct(out, @run_beg, @rbm_output, chn, false)
826        end
827      end
828      @rev_channels.times do |chn|
829        if @clm
830          (@run_beg...@run_end).each do |i|
831            out_any(i, out_data[i - @run_beg] * env(@reverb_gains[chn]), chn, @rbm_reverb)
832          end
833        else
834          out = vct_multiply!(vct_copy(out_data),
835                              Vct.new(len) do |x| env(@reverb_gains[chn]) end)
836          mix_vct(out, @run_beg, @rbm_reverb, chn, false)
837        end
838      end
839    end
840
841    # :amplitude_panning
842    # :b_format_ambisonics
843    # :decoded_ambisonics
844    def make_dlocsig(startime, dur, *args)
845      path, scaler, reverb_amount, output_power, reverb_power, render_using = nil
846      rbm_output, rbm_reverb, out_channels, rev_channels, clm = nil
847      optkey(args, binding,
848             :path,
849             [:scaler, 1.0],
850             [:reverb_amount, 0.05],
851             [:output_power, 1.5],
852             [:reverb_power, 0.5],
853             [:render_using, Amplitude_panning],
854             [:rbm_output, $output],
855             [:rbm_reverb, $reverb],
856             [:out_channels, 4],
857             [:rev_channels, 1],
858             [:clm, true])
859      @output_power = output_power
860      @reverb_power = reverb_power
861      @render_using = which_render(render_using)
862      @rbm_output   = rbm_output
863      @rbm_reverb   = rbm_reverb
864      @out_channels = out_channels
865      @rev_channels = rev_channels
866      @clm          = clm
867      if @render_using == B_format_ambisonics and @out_channels != 4
868        dl_error("B_format_ambisonics requires 4 output channels")
869      end
870      if @render_using == B_format_ambisonics and
871          @rev_channels.nonzero? and
872          (@rev_channels != 1 and @rev_channels != 4)
873        dl_error("B_format_ambisonics accepts only 0, 1 or 4 rev_channels")
874      end
875      if @rev_channels > @out_channels
876        dl_error("more rev_channels than out_channels")
877      end
878      if @render_using == B_format_ambisonics
879        scaler *= 0.8
880      end
881      unless path.kind_of?(Path)
882        if array?(path) and !path.empty?
883          path = make_path(path)
884        else
885          dl_error(path, "sorry, need a path")
886        end
887      end
888      xpoints = path.path_x
889      ypoints = path.path_y
890      zpoints = path.path_z
891      tpoints = path.path_time
892      @channel_gains = make_array(@out_channels) do [] end
893      @channel_rev_gains = make_array(@rev_channels) do [] end
894      self.set_speakers(@out_channels, (not zpoints.detect do |x| x.nonzero? end.nil?))
895      @speed_limit = (@speed_of_sound * (tpoints[-1] - tpoints[0])) / dur
896      if xpoints.length == 1
897        walk_all_rooms(xpoints[0], ypoints[0], zpoints[0], tpoints[0])
898      else
899        xb = yb = zb = tb = 0.0
900        (tpoints.length - 1).times do |i|
901          xa, xb = xpoints[i, 2]
902          ya, yb = ypoints[i, 2]
903          za, zb = zpoints[i, 2]
904          ta, tb = tpoints[i, 2]
905          minimum_segment_length(xa, ya, za, ta, xb, yb, zb, tb)
906        end
907        walk_all_rooms(xb, yb, zb, tb)
908      end
909      @start = dist2samples(@first_dist - @min_dist)
910      @end = seconds2samples(startime + dur)
911      min_delay = dist2samples(@min_dist)
912      @run_beg = seconds2samples(startime)
913      @run_end = @end + dist2samples(@last_dist) - min_delay
914      real_dur = dur + dist2seconds(@last_dist - @first_dist)
915      min_dist_unity = [@min_dist, 1.0].max
916      unity_gain = scaler * min_dist_unity ** @output_power
917      @output_gains = make_array(@number) do |i|
918        make_env(:envelope, @channel_gains[i], :scaler, unity_gain, :duration, real_dur)
919      end
920      if @rev_channels.nonzero?
921        unity_rev_gain = reverb_amount * scaler * min_dist_unity ** @reverb_power
922        @reverb_gains = make_array(@rev_channels) do |i|
923          make_env(:envelope, @channel_rev_gains[i], :scaler, unity_rev_gain, :duration, real_dur)
924        end
925      end
926      @delays = make_env(:envelope, @delay, :offset, -min_delay, :duration, real_dur)
927      @path = make_delay(:size, 1, :max_size, [1, dist2samples(@max_dist)].max)
928      self
929    end
930
931    private
932    def dist2samples(d)
933      (d * (mus_srate() / @speed_of_sound)).round
934    end
935
936    def dist2seconds(d)
937      d / @speed_of_sound.to_f
938    end
939
940    def transition_point_3(vert_a, vert_b, xa, ya, za, xb, yb, zb)
941      line_b = vct(xa, ya, za)
942      line_m = tr3_sub(vct(xb, yb, zb), line_b)
943      normal = tr3_cross(vert_a, vert_b)
944      if (denominator = tr3_dot(normal, line_m)).abs <= 0.000001
945        false
946      else
947        vct2list(tr3_add(line_b, tr3_scale(line_m, -tr3_dot(normal, line_b) / denominator)))
948      end
949    end
950
951    def tr3_cross(v1, v2)
952      vct(v1[1] * v2[2] - v1[2] * v2[1],
953          v1[2] * v2[0] - v1[0] * v2[2],
954          v1[0] * v2[1] - v1[1] * v2[0])
955    end
956
957    def tr3_dot(v1, v2)
958      dot_product(v1, v2)
959    end
960
961    def tr3_sub(v1, v2)
962      vct_subtract!(vct_copy(v1), v2)
963    end
964
965    def tr3_add(v1, v2)
966      vct_add!(vct_copy(v1), v2)
967    end
968
969    def tr3_scale(v1, c)
970      vct_scale!(vct_copy(v1), c)
971    end
972
973    def transition_point_2(vert, xa, ya, xb, yb)
974      ax = vert[0]
975      bx = xa - xb
976      ay = vert[1]
977      by = ya - yb
978      cx = -xa
979      cy = -ya
980      d = by * cx - bx * cy
981      f = ay * bx - ax * by
982      if f.zero?
983        false
984      else
985        [(d * ax) / f, (d * ay) / f]
986      end
987    end
988
989    def calculate_gains(x, y, z, group)
990      zero_coord = 1e-10
991      zero_gain = 1e-10
992      size = group.size
993      if mat = group.matrix
994        if x.abs < zero_coord and y.abs < zero_coord and z.abs < zero_coord
995          [true, [1.0, 1.0, 1.0]]
996        else
997          case size
998          when 3
999            gain_a = mat[0][0] * x + mat[0][1] * y + mat[0][2] * z
1000            gain_b = mat[1][0] * x + mat[1][1] * y + mat[1][2] * z
1001            gain_c = mat[2][0] * x + mat[2][1] * y + mat[2][2] * z
1002            mag = distance(gain_a, gain_b, gain_c)
1003            if gain_a.abs < zero_gain then gain_a = 0.0 end
1004            if gain_b.abs < zero_gain then gain_b = 0.0 end
1005            if gain_c.abs < zero_gain then gain_c = 0.0 end
1006            [(gain_a >= 0 and gain_b >= 0 and gain_c >= 0),
1007              [gain_a / mag, gain_b / mag, gain_c / mag]]
1008          when 2
1009            gain_a = mat[0][0] * x + mat[0][1] * y
1010            gain_b = mat[1][0] * x + mat[1][1] * y
1011            mag = distance(gain_a, gain_b, 0.0)
1012            if gain_a.abs < zero_gain then gain_a = 0.0 end
1013            if gain_b.abs < zero_gain then gain_b = 0.0 end
1014            [(gain_a >= 0 and gain_b >= 0), [gain_a / mag, gain_b / mag]]
1015          when 1
1016            [true, [1.0]]
1017          end
1018        end
1019      else
1020        [true, [1.0, 1.0, 1.0]]
1021      end
1022    end
1023
1024    def find_group(x, y, z)
1025      grp = gns = false
1026      @groups.detect do |group|
1027        inside, gains = calculate_gains(x, y, z, group)
1028        if inside
1029          grp, gns = group, gains
1030          true
1031        end
1032      end
1033      [grp, gns]
1034    end
1035
1036    def push_zero_gains(time)
1037      @out_channels.times do |i| @channel_gains[i].push(time, 0.0) end
1038      @rev_channels.times do |i| @channel_rev_gains[i].push(time, 0.0) end
1039    end
1040
1041    def push_gains(group, gains, dist, time)
1042      outputs = make_vct(@out_channels)
1043      revputs = if @rev_channels > 0
1044                  make_vct(@rev_channels)
1045                else
1046                  false
1047                end
1048      if dist >= 1.0
1049        att = 1.0 / dist ** @output_power
1050        ratt = 1.0 / dist ** @reverb_power
1051      else
1052        att = 1.0 - dist ** (1.0 / @output_power)
1053        ratt = 1.0 - dist ** (1.0 / @reverb_power)
1054      end
1055      if dist >= 1.0
1056        group.speakers.each_with_index do |speaker, i|
1057          gain = gains[i]
1058          outputs[speaker] = gain * att
1059          if @rev_channels > 1
1060            revputs[speaker] = gain * ratt
1061          end
1062        end
1063      else
1064        @number.times do |speaker|
1065          if found = group.speakers.index(speaker)
1066            gain = gains[found]
1067            outputs[speaker] = gain + (1.0 - gain) * att
1068            if @rev_channels > 1
1069              revputs[speaker] = gain + (1.0 - gain) * ratt
1070            end
1071          else
1072            outputs[speaker] = att
1073            if @rev_channels > 1
1074              revputs[speaker] = ratt
1075            end
1076          end
1077        end
1078      end
1079      vct2list(outputs).each_with_index do |val, i| @channel_gains[i].push(time, val) end
1080      if @rev_channels == 1
1081        @channel_rev_gains[0].push(time, ratt)
1082      elsif @rev_channels > 1
1083        vct2list(revputs).each_with_index do |val, i| @channel_rev_gains[i].push(time, val) end
1084      end
1085    end
1086
1087    def amplitude_panning(x, y, z, dist, time)
1088      if @prev_group
1089        if time != @prev_time and ((dist - @prev_dist) / (time - @prev_time)) > @speed_limit
1090          Snd.display("%s#%s: supersonic radial movement", self.class, get_func_name)
1091        end
1092        inside, gains = calculate_gains(x, y, z, @prev_group)
1093        if inside
1094          push_gains(@prev_group, gains, dist, time)
1095          @prev_x, @prev_y, @prev_z = x, y, z
1096        else
1097          group, gains = find_group(x, y, z)
1098          if group
1099            edge = group.vertices & @prev_group.vertices
1100            if edge.length == 2
1101              if pint = transition_point_3(edge[0], edge[1], x, y, z, @prev_x, @prev_y, @prev_z)
1102                xi, yi, zi = pint
1103                di = distance(xi, yi, zi)
1104                ti = @prev_time +
1105                     (distance(xi - @prev_x, yi - @prev_y, zi - @prev_z) / \
1106                      distance(x - @prev_x, y - @prev_y, z - @prev_z)) * (time - @prev_time)
1107                if ti < @prev_time
1108                  inside, gains = calculate_gains(xi, yi, zi, @prev_group)
1109                  if inside
1110                    push_gains(@prev_group, gains, di, ti)
1111                  else
1112                    inside, gains = calculate_gains(xi, yi, zi, group)
1113                    if inside
1114                      push_gains(group, gains, di, ti)
1115                    else
1116                      dl_error("outside of both adjacent groups")
1117                    end
1118                  end
1119                else
1120                  if $DEBUG
1121                    Snd.warning("%s#%s: current time <= previous time", self.class, get_func_name)
1122                  end
1123                end
1124              end
1125            elsif edge.length == 1 and group.size == 2
1126              if pint = transition_point_2(edge[0], x, y, @prev_x, @prev_y)
1127                xi, yi = pint
1128                di = distance(xi, yi, 0.0)
1129                ti = @prev_time +
1130                     (distance(xi - @prev_x, yi - @prev_y, 0.0) / \
1131                      distance(x - @prev_x, y - @prev_y, 0.0)) * (time - @prev_time)
1132                if ti < @prev_time
1133                  inside, gains = calculate_gains(xi, yi, 0.0, @prev_group)
1134                  if inside
1135                    push_gains(@prev_group, gains, di, ti)
1136                    inside, gains = calculate_gains(xi, yi, 0.0, group)
1137                    if inside
1138                      push_gains(group, gains, di, ti)
1139                    else
1140                      dl_error("outside of both adjacent groups")
1141                    end
1142                  end
1143                else
1144                  if $DEBUG
1145                    Snd.warning("%s#%s: current time <= previous time", self.class, get_func_name)
1146                  end
1147                end
1148              end
1149            elsif edge.length == 1
1150              Snd.display("%s#%s: only one point in common", self.class, get_func_name)
1151            elsif edge.length.zero?
1152              Snd.display("%s#%s: with no common points", self.class, get_func_name)
1153            end
1154            push_gains(group, gains, dist, time)
1155            @prev_group, @prev_x, @prev_y, @prev_z = group, x, y, z
1156          else
1157            push_zero_gains(time)
1158            @prev_group = false
1159          end
1160        end
1161      else
1162        group, gains = find_group(x, y, z)
1163        if group
1164          push_gains(group, gains, dist, time)
1165          @prev_group, @prev_x, @prev_y, @prev_z = group, x, y, z
1166        else
1167          push_zero_gains(time)
1168          @prev_group = false
1169        end
1170      end
1171      @prev_time = time
1172      @prev_dist = dist
1173    end
1174
1175    def b_format_ambisonics(x, y, z, dist, time)
1176      if dist > 1.0
1177        att = (1.0 / dist) ** @output_power
1178        @channel_gains[0].push(time, Point707 * att)
1179        @channel_gains[1].push(time, (y / dist) * att)
1180        @channel_gains[2].push(time, (-x / dist) * att)
1181        @channel_gains[3].push(time, (z / dist) * att)
1182        if @rev_channels == 1
1183          @channel_rev_gains[0].push(time, 1.0 / (dist ** @reverb_power))
1184        elsif @rev_channels == 4
1185          ratt = (1.0 / dist) ** @reverb_power
1186          @channel_rev_gains[0].push(time, Point707 * ratt)
1187          @channel_rev_gains[1].push(time, (y / dist) * ratt)
1188          @channel_rev_gains[2].push(time, (-x / dist) * ratt)
1189          @channel_rev_gains[3].push(time, (z / dist) * ratt)
1190        end
1191      elsif dist.zero?
1192        @channel_gains[0].push(time, 1.0)
1193        (1..3).each do |i| @channel_gains[i].push(time, 0.0) end
1194        if @rev_channels >= 1
1195          @channel_rev_gains[0].push(time, 1.0)
1196        end
1197        if @rev_channels == 4
1198          (1..3).each do |i| @channel_rev_gains[i].push(time, 0.0) end
1199        end
1200      else
1201        att = dist ** (1.0 / @output_power)
1202        @channel_gains[0].push(time, 1.0 - (1.0 - Point707) * dist ** @output_power)
1203        @channel_gains[1].push(time, (y / dist) * att)
1204        @channel_gains[2].push(time, (-x / dist) * att)
1205        @channel_gains[3].push(time, (z / dist) * att)
1206        if @rev_channels == 1
1207          @channel_rev_gains[0].push(time, 1.0 - dist ** (1.0 / @reverb_power))
1208        elsif @rev_channels == 4
1209          ratt = dist ** (1.0 / @reverb_power)
1210          @channel_rev_gains[0].push(time, 1.0 - (1.0 - Point707) * dist ** @reverb_power)
1211          @channel_rev_gains[1].push(time, (y / dist) * ratt)
1212          @channel_rev_gains[2].push(time, (-x / dist) * ratt)
1213          @channel_rev_gains[3].push(time, (z / dist) * ratt)
1214        end
1215      end
1216    end
1217
1218    def decoded_ambisonics(x, y, z, dist, time)
1219      if dist > 1.0
1220        att = (1.0 / dist) ** @output_power
1221        attw = Point707 * Point707 * att
1222        attx = att * (x / dist)
1223        atty = att * (y / dist)
1224        attz = att * (z / dist)
1225        @coords.each_with_index do |s, i|
1226          @channel_gains[i].push(time, Point707 * (attw + attx * s[0] + atty * s[1] + attz * s[2]))
1227        end
1228        if @rev_channels == 1
1229          @channel_rev_gains[0].push(time, 1.0 / (dist ** @reverb_power))
1230        elsif @rev_channels == 4
1231          ratt = (1.0 / dist) ** @reverb_power
1232          rattw = Point707 * Point707 * ratt
1233          rattx = ratt * (x / dist)
1234          ratty = ratt * (y / dist)
1235          rattz = ratt * (z / dist)
1236          @rev_channels.times do |i|
1237            s = @coords[i]
1238            @channel_rev_gains[i].push(time, Point707 * \
1239                                       (rattw + rattx * s[0] + ratty * s[1] + rattz * s[2]))
1240          end
1241        end
1242      elsif dist.zero?
1243        att = Point707 * Point707
1244        @coords.each_index do |i| @channel_gains[i].push(time, att) end
1245        if @rev_channels == 1
1246          @channel_rev_gains[0].push(time, 1.0)
1247        else
1248          @rev_channels.times do |i| @channel_rev_gains[i].push(time, att) end
1249        end
1250      else
1251        att = dist ** (1.0 / @output_power)
1252        attw = Point707 * (1.0 - (1.0 - Point707) * dist ** @output_power)
1253        attx = att * (x / dist)
1254        atty = att * (y / dist)
1255        attz = att * (z / dist)
1256        @coords.each_with_index do |s, i|
1257          @channel_gains[i].push(time, Point707 * (attw + attx * s[0] + atty * s[1] + attz * s[2]))
1258        end
1259        if @rev_channels == 1
1260          @channel_rev_gains[0].push(time, 1.0 - dist ** (1.0 / @reverb_power))
1261        elsif @rev_channels == 4
1262          ratt = dist ** (1.0 / @reverb_power)
1263          rattw = Point707 * (1.0 - (1.0 - Point707) * dist ** @reverb_power)
1264          rattx = ratt * (x / dist)
1265          ratty = ratt * (y / dist)
1266          rattz = ratt * (z / dist)
1267          @rev_channels.times do |i|
1268            s = @coords[i]
1269            @channel_rev_gains[i].push(time, Point707 * \
1270                                       (rattw + rattx * s[0] + ratty * s[1] + rattz * s[2]))
1271          end
1272        end
1273      end
1274    end
1275
1276    def walk_all_rooms(x, y, z, time)
1277      dist = distance(x, y, z)
1278      if @first_dist.zero?
1279        @first_dist = dist
1280      end
1281      @last_dist = dist
1282      if @min_dist.zero? or dist < @min_dist
1283        @min_dist = dist
1284      end
1285      if @max_dist.zero? or dist > @max_dist
1286        @max_dist = dist
1287      end
1288      @delay.push(time, dist2samples(dist))
1289      case @render_using
1290      when Amplitude_panning
1291        amplitude_panning(x, y, z, dist, time)
1292      when B_format_ambisonics
1293        b_format_ambisonics(x, y, z, dist, time)
1294      when Decoded_ambisonics
1295        decoded_ambisonics(x, y, z, dist, time)
1296      end
1297    end
1298
1299    def change_direction(xa, ya, za, ta, xb, yb, zb, tb)
1300      walk_all_rooms(xa, ya, za, ta)
1301      if xa != xb or ya != yb or za != zb or ta != tb
1302        xi, yi, zi = nearest_point(xa, ya, za, xb, yb, zb, 0.0, 0.0, 0.0)
1303        if (((xa < xb) ? (xa <= xi and xi <= xb) : (xb <= xi and xi <= xa)) and
1304            ((ya < yb) ? (ya <= yi and yi <= yb) : (yb <= yi and yi <= ya)) and
1305            ((za < zb) ? (za <= zi and zi <= zb) : (zb <= zi and zi <= za)))
1306          walk_all_rooms(xi, yi, zi,
1307                         tb + (ta - tb) * (distance(xb - xi, yb - yi, zb - zi) / \
1308                                           distance(xb - xa, yb - ya, zb - za)))
1309        end
1310      end
1311    end
1312
1313    def intersects_inside_radius(xa, ya, za, ta, xb, yb, zb, tb)
1314      mag = distance(xb - xa, yb - ya, zb - za)
1315      vx = (xb - xa) / mag
1316      vy = (yb - ya) / mag
1317      vz = (zb - za) / mag
1318      bsq = xa * vx + ya * vy + za * vz
1319      disc = bsq * bsq - ((xa * xa + ya * ya + za * za) - 1.0)
1320      if disc >= 0.0
1321        root = sqrt(disc)
1322        rin = -bsq - root
1323        rout = -bsq + root
1324        xi = xo = nil
1325        if rin > 0 and rin < mag
1326          xi = xa + vx * rin
1327          yi = ya + vy * rin
1328          zi = za + vz * rin
1329          ti = tb + (ta - tb) *
1330               (distance(xb - xi, yb - yi, zb - zi) / distance(xb - xa, yb - ya, zb - za))
1331        end
1332        if rout > 0 and rout.abs < mag
1333          xo = xa + vx * rout
1334          yo = ya + vy * rout
1335          zo = za + vz * rout
1336          to = tb + (ta - tb) *
1337               (distance(xb - xo, yb - yo, zb - zo) / distance(xb - xa, yb - ya, zb - za))
1338        end
1339        if xi
1340          change_direction(xa, ya, za, ta, xi, yi, zi, ti)
1341          if xo
1342            change_direction(xi, yi, zi, ti, xo, yo, zo, to)
1343            change_direction(xo, yo, zo, to, xb, yb, zb, tb)
1344          else
1345            change_direction(xi, yi, zi, ti, xb, yb, zb, tb)
1346          end
1347        else
1348          if xo
1349            change_direction(xa, ya, za, ta, xo, yo, zo, to)
1350            change_direction(xo, yo, zo, to, xb, yb, zb, tb)
1351          else
1352            change_direction(xa, ya, za, ta, xb, yb, zb, tb)
1353          end
1354        end
1355      else
1356        change_direction(xa, ya, za, ta, xb, yb, zb, tb)
1357      end
1358    end
1359
1360    def minimum_segment_length(xa, ya, za, ta, xb, yb, zb, tb)
1361      if distance(xb - xa, yb - ya, zb - za) < 1.0
1362        intersects_inside_radius(xa, ya, za, ta, xb, yb, zb, tb)
1363      else
1364        xi = (xa + xb) * 0.5
1365        yi = (ya + yb) * 0.5
1366        zi = (za + zb) * 0.5
1367        ti = tb + (ta - tb) *
1368             (distance(xb - xi, yb - yi, zb - zi) / distance(xb - xa, yb - ya, zb - za))
1369        minimum_segment_length(xa, ya, za, ta, xi, yi, zi, ti)
1370        minimum_segment_length(xi, yi, zi, ti, xb, yb, zb, tb)
1371      end
1372    end
1373  end
1374
1375  class Path < Dlocsig_base
1376    def initialize
1377      super
1378      @rx = @ry = @rz = @rv = @rt = @tx = @ty = @tz = @tt = nil
1379      @gnuplot = @sndplot = nil
1380    end
1381
1382    def path_x
1383      (@tx or (@rx or (render_path(); @rx)))
1384    end
1385
1386    def path_y
1387      (@ty or (@ry or (render_path(); @ry)))
1388    end
1389
1390    def path_z
1391      (@tz or (@rz or (render_path(); @rz)))
1392    end
1393
1394    def path_time
1395      (@tt or (@rt or (render_path(); @rt)))
1396    end
1397
1398    def scale_path(scaling)
1399      assert_type((number?(scaling) or array?(scaling)), 0, scaling, "a number or an array")
1400      if number?(scaling) then scaling = [scaling, scaling, scaling] end
1401      transform_path(:scaling, scaling)
1402    end
1403
1404    def translate_path(translation)
1405      assert_type((number?(translation) or array?(translation)),
1406                  0, translation, "a number or an array")
1407      if number?(translation) then translation = [translation, translation, translation] end
1408      transform_path(:translation, translation)
1409    end
1410
1411    def rotate_path(rotation, *args)
1412      assert_type(number?(rotation), 0, rotation, "a number")
1413      rotation_center, rotation_axis = nil
1414      optkey(args, binding,
1415             :rotation_center,
1416             [:rotation_axis, [0.0, 0.0, 1.0]])
1417      transform_path(:rotation, rotation,
1418                     :rotation_center, rotation_center,
1419                     :rotation_axis, rotation_axis)
1420    end
1421
1422    def path_trajectory
1423      path_x.map_with_index do |d, i| [d, path_y[i], path_z[i]] end.flatten
1424    end
1425
1426    def path_2d_trajectory
1427      path_x.map_with_index do |d, i| [d, path_y[i]] end.flatten
1428    end
1429
1430    # if velocity is zero, ti == tf
1431    Secure_distance = 0.0001
1432    def path_velocity
1433      xp, yp, zp, tp = path_x, path_y, path_z, path_time
1434      (0...(tp.length - 1)).map do |i|
1435        xi, xf = xp[i, 2]
1436        yi, yf = yp[i, 2]
1437        zi, zf = zp[i, 2]
1438        ti, tf = tp[i, 2]
1439        if tf == ti
1440          tf += Secure_distance
1441        end
1442        [(ti + tf) / 2.0, distance(xf - xi, yf - yi, zf - zi) / (tf - ti)]
1443      end.flatten
1444    end
1445
1446    def path_doppler
1447      xp, yp, zp, tp = path_x, path_y, path_z, path_time
1448      (0...(tp.length - 1)).map do |i|
1449        xi, xf = xp[i, 2]
1450        yi, yf = yp[i, 2]
1451        zi, zf = zp[i, 2]
1452        ti, tf = tp[i, 2]
1453        if tf == ti
1454          tf += Secure_distance
1455        end
1456        [(tf + ti) / 2.0, -((distance(xf, yf, zf) - distance(xi, yi, zi)) / (tf - ti))]
1457      end.flatten
1458    end
1459
1460    def path_acceleration
1461      v = path_velocity()
1462      result = []
1463      0.step(v.length - 3, 2) do |i|
1464        ti, vi, tf, vf = v[i, 4]
1465        if tf == ti
1466          tf += Secure_distance
1467        end
1468        am = (vf - vi) / (tf - ti)
1469        result << ti << am << tf << am
1470      end
1471      result
1472    end
1473
1474    # Gnuplot
1475    def plot_open
1476      if @gnuplot.kind_of?(Gnuplot)
1477        @gnuplot
1478      else
1479        @gnuplot = Gnuplot.new
1480      end
1481    end
1482
1483    def plot_close
1484      @gnuplot.close if @gnuplot.kind_of?(Gnuplot)
1485    end
1486
1487    def cmd(*args)
1488      @gnuplot = plot_open
1489      @gnuplot.command(format(*args) << "\n")
1490    end
1491
1492    def plot_trajectory(*args)
1493      @gnuplot = plot_open
1494      label, reset = nil
1495      optkey(args, binding,
1496             [:label, "trajectory"],
1497             [:reset, true])
1498      @gnuplot.reset() if reset
1499      @gnuplot.set_autoscale()
1500      if path_z.detect do |z| z.nonzero? end
1501        @gnuplot.plot_3d_curve(path_trajectory(), :label, label, *args)
1502      else
1503        @gnuplot.plot_2d_curve(path_2d_trajectory(), :label, label, *args)
1504      end
1505    end
1506
1507    def plot_velocity(reset = true)
1508      @gnuplot = plot_open
1509      @gnuplot.reset() if reset
1510      @gnuplot.set_autoscale()
1511      @gnuplot.plot_2d_curve(path_velocity(), :label, "velocity", :style, "steps")
1512    end
1513
1514    def plot_doppler(reset = true)
1515      @gnuplot = plot_open
1516      @gnuplot.reset() if reset
1517      @gnuplot.set_autoscale()
1518      @gnuplot.plot_2d_curve(path_doppler(), :label, "doppler", :style, "steps")
1519    end
1520
1521    def plot_acceleration(reset = true)
1522      @gnuplot = plot_open
1523      @gnuplot.reset() if reset
1524      @gnuplot.set_autoscale()
1525      @gnuplot.plot_2d_curve(path_acceleration(), :label, "acceleration", :style, "steps")
1526    end
1527
1528    def pplot(normalize = true)
1529      @gnuplot = plot_open
1530      norm = lambda do |env, nrm|
1531        unless nrm
1532          env
1533        else
1534          mx = env.each_pair do |x, y| y end.max
1535          if mx.zero?
1536            env
1537          else
1538            env.each_pair do |x, y| [x, y / mx.to_f] end.flatten
1539          end
1540        end
1541      end
1542      @gnuplot.reset()
1543      @gnuplot.size(0, 0, 1, 1)
1544      @gnuplot.start_multiplot()
1545      @gnuplot.size(0.0, 0.333, 1.0, 0.667)
1546      plot_trajectory(:reset, false)
1547      @gnuplot.size(0.0, 0.0, 1.0, 0.333)
1548      @gnuplot.plot_2d_curves([norm.call(path_velocity(), normalize),
1549                               norm.call(path_acceleration(), normalize),
1550                               norm.call(path_doppler(), normalize)],
1551                              :labels, ["velocity", "acceleration", "doppler"],
1552                              :styles, ["steps", "steps", "steps"])
1553      @gnuplot.end_multiplot()
1554    end
1555
1556    # Sndplot
1557    def snd_open(chns = 1)
1558      if @sndplot.kind_of?(Sndplot)
1559        @sndplot
1560      else
1561        @sndplot = Sndplot.new(chns)
1562      end
1563    end
1564
1565    def snd_close
1566      @sndplot.close if @sndplot.kind_of?(Sndplot)
1567    end
1568
1569    def snd_trajectory(chn = 0, label = "trajectory")
1570      @sndplot = snd_open
1571      graph(path_2d_trajectory(), label, false, false, false, false, @sndplot.snd, chn)
1572      @sndplot
1573    end
1574
1575    def snd_velocity(chn = 0, label = "velocity")
1576      @sndplot = snd_open
1577      graph(path_velocity(), label, false, false, false, false, @sndplot.snd, chn)
1578      @sndplot
1579    end
1580
1581    def snd_doppler(chn = 0, label = "doppler")
1582      @sndplot = snd_open
1583      graph(path_doppler(), label, false, false, false, false, @sndplot.snd, chn)
1584      @sndplot
1585    end
1586
1587    def snd_acceleration(chn = 0, label = "acceleration")
1588      @sndplot = snd_open
1589      graph(path_acceleration(), label, false, false, false, false, @sndplot.snd, chn)
1590      @sndplot
1591    end
1592
1593    def snd_plot
1594      @sndplot = snd_open(4)
1595      snd_trajectory(0)
1596      snd_velocity(1)
1597      snd_acceleration(2)
1598      snd_doppler(3)
1599      @sndplot
1600    end
1601
1602    private
1603    def transform_path(*args)
1604      scaling, translation, rotation, rotation_center, rotation_axis = nil
1605      optkey(args, binding,
1606             :scaling,
1607             :translation,
1608             :rotation,
1609             :rotation_center,
1610             [:rotation_axis, [0.0, 0.0, 1.0]])
1611      render_path() if @rx.nil?
1612      if scaling or translation or rotation
1613        rotation = TWO_PI * (rotation / @one_turn) if rotation
1614        if rotation_axis and (rotation_axis.length != 3)
1615          dl_error(rotation_axis, "rotation axis has to have all three coordinates")
1616        end
1617        matrix = if rotation
1618                   rotation_matrix(rotation_axis[0], rotation_axis[1], rotation_axis[2], rotation)
1619                 end
1620        xc = path_x()
1621        yc = path_y()
1622        zc = path_z()
1623        if rotation_center and (rotation_center.length != 3)
1624          dl_error(rotation_center, "rotation center has to have all three coordinates")
1625        end
1626        xtr = []
1627        ytr = []
1628        ztr = []
1629        xc.each_with_index do |x, i|
1630          y = yc[i]
1631          z = zc[i]
1632          xw, yw, zw = x, y, z
1633          if rotation_center and rotation
1634            xw -= rotation_center[0]
1635            yw -= rotation_center[1]
1636            zw -= rotation_center[2]
1637          end
1638          if rotation
1639            mc = [xw, yw, zw]
1640            xv, yv, zv = matrix.column_vectors
1641            xr = xv.to_a.map_with_index do |xx, ii| xx * mc[ii] end.to_a.sum
1642            yr = yv.to_a.map_with_index do |xx, ii| xx * mc[ii] end.to_a.sum
1643            zr = zv.to_a.map_with_index do |xx, ii| xx * mc[ii] end.to_a.sum
1644            xw, yw, zw = xr, yr, zr
1645          end
1646          if rotation_center and rotation
1647            xw += rotation_center[0]
1648            yw += rotation_center[1]
1649            zw += rotation_center[2]
1650          end
1651          if scaling
1652            xw *= scaling[0]
1653            yw *= scaling[1] if scaling[1]
1654            zw *= scaling[2] if scaling[2]
1655          end
1656          if translation
1657            xw += translation[0]
1658            yw += translation[1] if translation[1]
1659            zw += translation[2] if translation[2]
1660          end
1661          xtr << xw
1662          ytr << yw
1663          ztr << zw
1664        end
1665        @tx, @ty, @tz = xtr, ytr, ztr
1666      else
1667        @tt = @rt.dup
1668        @tx = @rx.dup
1669        @ty = @ry.dup
1670        @tz = @rz.dup
1671      end
1672    end
1673
1674    def reset_transformation
1675      @tt = @tx = @ty = @tz = nil
1676    end
1677
1678    def reset_rendering
1679      @rt = @rv = @rx = @ry = @rz = nil
1680      reset_transformation()
1681    end
1682
1683    def parse_cartesian_coordinates(points, d3)
1684      if array?(points[0])
1685        x = points.map do |p| p[0] end
1686        y = points.map do |p| p[1] end
1687        z = points.map do |p| d3 ? (p[2] or 0.0) : 0.0 end
1688        v = points.map do |p| d3 ? p[3] : p[2] end
1689        [x, y, z, v]
1690      else
1691        if d3
1692          x = []
1693          y = []
1694          z = []
1695          0.step(points.length - 3, 3) do |i|
1696            x += [points[i]]
1697            y += [points[i + 1]]
1698            z += [points[i + 2]]
1699          end
1700          [x, y, z, x.map do |i| nil end]
1701        else
1702          x = []
1703          y = []
1704          0.step(points.length - 2, 2) do |i|
1705            x += [points[i]]
1706            y += [points[i + 1]]
1707          end
1708          [x, y, x.map do |i| 0.0 end, x.map do |i| nil end]
1709        end
1710      end
1711    end
1712
1713    def parse_polar_coordinates(points, d3)
1714      if array?(points[0])
1715        x = []
1716        y = []
1717        z = []
1718        v = []
1719        points.each do |p|
1720          d = p[0]
1721          a = p[1]
1722          e = (d3 ? (p[2] or 0.0) : 0.0)
1723          evec = cis((e / @one_turn) * TWO_PI)
1724          dxy = d * evec.real
1725          avec = cis((a / @one_turn) * TWO_PI)
1726          z << (d * evec.imag)
1727          x << (dxy * avec.imag)
1728          y << (dxy * avec.real)
1729          v << (d3 ? p[3] : p[2])
1730        end
1731        [x, y, z, v]
1732      else
1733        if d3
1734          x = []
1735          y = []
1736          z = []
1737          0.step(points.length - 1, 3) do |i|
1738            d, a, e = points[i, 3]
1739            evec = cis((e / @one_turn) * TWO_PI)
1740            dxy = (d * evec.real)
1741            avec = cis((a / @one_turn) * TWO_PI)
1742            z << (d * evec.imag)
1743            x << (dxy * avec.imag)
1744            y << (dxy * avec.real)
1745          end
1746          [x, y, z, x.map do |i| nil end]
1747        else
1748          x = []
1749          y = []
1750          0.step(points.length - 1, 2) do |i|
1751            d, a = points[i, 2]
1752            avec = cis((a / @one_turn) * TWO_PI)
1753            x << (d * avec.imag)
1754            y << (d * avec.real)
1755          end
1756          [x, y, x.map do |i| 0.0 end, x.map do |i| nil end]
1757        end
1758      end
1759    end
1760  end
1761
1762  class Bezier_path < Path
1763    def initialize(path, *args)
1764      @path = path
1765      if (not @path) or (array?(@path) and @path.empty?)
1766        dl_error("can't define a path with no points in it")
1767      end
1768      super()
1769      d3, polar, error, curvature = nil
1770      optkey(args, binding,
1771             [:d3, true],
1772             [:polar, false],
1773             [:error, 0.01],
1774             :curvature)
1775      @d3        = d3
1776      @polar     = polar
1777      @error     = error
1778      @curvature = curvature
1779      @x = @y = @z = @v = @bx = @by = @bz = nil
1780      # for ac() and a()
1781      @path_ak_even = nil
1782      @path_ak_odd = nil
1783      @path_gtab = nil
1784      @path_ftab = nil
1785    end
1786
1787    private
1788    def parse_path
1789      if @polar
1790        @x, @y, @z, @v = parse_polar_coordinates(@path, @d3)
1791      else
1792        @x, @y, @z, @v = parse_cartesian_coordinates(@path, @d3)
1793      end
1794      if @v[0] and @v.min < 0.1
1795        if @v.min < 0.0
1796          Snd.warning("%s#%s: velocities must be all positive, corrected",
1797                      self.class, get_func_name)
1798        end
1799        @v.map! do |x| [0.1, x].max end
1800      end
1801      @bx = @by = @bz = nil
1802      reset_rendering()
1803    end
1804
1805    def fit_path
1806      parse_path() if @x.nil?
1807    end
1808
1809    def bezier_point(u, c)
1810      u1 = 1.0 - u
1811      cr = make_array(3) do |i| make_array(3) do |j| u1 * c[i][j] + u * c[i][j + 1] end end
1812      1.downto(0) do |i|
1813        0.upto(i) do |j|
1814          3.times do |k| cr[k][j] = u1 * cr[k][j] + u * cr[k][j + 1] end
1815        end
1816      end
1817      [cr[0][0], cr[1][0], cr[2][0]]
1818    end
1819
1820    def berny(xl, yl, zl, xh, yh, zh, ul, u, uh, c)
1821      x, y, z = bezier_point(u, c)
1822      xn, yn, zn = nearest_point(xl, yl, zl, xh, yh, zh, x, y, z)
1823      if distance(xn - x, yn - y, zn - z) > @error
1824        xi, yi, zi = berny(xl, yl, zl, x, y, z, ul, (ul + u) / 2.0, u, c)
1825        xj, yj, zj = berny(x, y, z, xh, yh, zh, u, (u + uh) / 2.0, uh, c)
1826        [xi + [x] + xj, yi + [y] + yj, zi + [z] + zj]
1827      else
1828        [[], [], []]
1829      end
1830    end
1831
1832    def render_path
1833      fit_path() if @bx.nil?
1834      rx = []
1835      ry = []
1836      rz = []
1837      rv = []
1838      if (not @v[0]) or @v[0].zero?
1839        @v[0] = 1.0
1840        @v[-1] = 1.0
1841      end
1842      if @x.length == 1
1843        @rx = @x
1844        @ry = @y
1845        @rz = @z
1846        @rt = [0.0]
1847        return
1848      end
1849      xf_bz = yf_bz = zf_bz = vf_bz = 0.0
1850      (@v.length - 1).times do |i|
1851        x_bz = @bx[i]
1852        y_bz = @by[i]
1853        z_bz = @bz[i]
1854        vi_bz, vf_bz = @v[i, 2]
1855        xi_bz = x_bz[0]
1856        xf_bz = x_bz[-1]
1857        yi_bz = y_bz[0]
1858        yf_bz = y_bz[-1]
1859        zi_bz = z_bz[0]
1860        zf_bz = z_bz[-1]
1861        xs, ys, zs = berny(xi_bz, yi_bz, zi_bz, xf_bz, yf_bz, zf_bz, 0, 0.5, 1, [x_bz, y_bz, z_bz])
1862        rx += [xi_bz] + xs
1863        ry += [yi_bz] + ys
1864        rz += [zi_bz] + zs
1865        rv += [vi_bz] + xs.map do nil end
1866      end
1867      rx << xf_bz
1868      ry << yf_bz
1869      rz << zf_bz
1870      rv << vf_bz
1871      xseg = [rx[0]]
1872      yseg = [ry[0]]
1873      zseg = [rz[0]]
1874      vi = rv[0]
1875      ti = 0.0
1876      times = [ti]
1877      (1...rx.length).each do |i|
1878        x = rx[i]
1879        y = ry[i]
1880        z = rz[i]
1881        v = rv[i]
1882        xseg << x
1883        yseg << y
1884        zseg << z
1885        if v
1886          sofar = 0.0
1887          dseg = (0...xseg.length - 1).map do |j|
1888            xsi, xsf = xseg[j, 2]
1889            ysi, ysf = yseg[j, 2]
1890            zsi, zsf = zseg[j, 2]
1891            sofar += distance(xsf - xsi, ysf - ysi, zsf - zsi)
1892          end
1893          df = dseg[-1]
1894          vf = v
1895          aa = ((vf - vi) * (vf + vi)) / (df * 4.0)
1896          tseg = dseg.map do |d|
1897            ti + (if vi and vi.nonzero? and vf == vi
1898                    d / vi
1899                  elsif aa.nonzero?
1900                    ((vi * vi + 4.0 * aa * d) ** 0.5 - vi) / (2.0 * aa)
1901                  else
1902                    0.0
1903                  end)
1904          end
1905          times += tseg
1906          xseg = [x]
1907          yseg = [y]
1908          zseg = [z]
1909          vi = v
1910          ti = tseg[-1]
1911        end
1912      end
1913      @rx = rx
1914      @ry = ry
1915      @rz = rz
1916      tf = times[-1]
1917      @rt = times.map do |tii| tii / tf end
1918      reset_transformation()
1919    end
1920
1921    # called in Closed_bezier_path#calculate_fit
1922    def a(k, n)
1923      if ([Path_maxcoeff * 2.0 + 1, n].min).odd?
1924        make_a_odd() unless @path_ak_odd
1925        @path_ak_odd[(n - 3) / 2][k - 1]
1926      else
1927        make_a_even() unless @path_ak_even
1928        @path_ak_even[(n - 4) / 2][k - 1]
1929      end
1930    end
1931
1932    # called in Open_bezier_path#calculate_fit
1933    def ac(k, n)
1934      n = [n, Path_maxcoeff].min
1935      make_a_even() unless @path_ak_even
1936      @path_ak_even[n - 2][k - 1]
1937    end
1938
1939    def make_a_even
1940      g = lambda do |m|
1941        @path_gtab = make_array(Path_maxcoeff) unless @path_gtab
1942        @path_gtab[0] = 1.0
1943        @path_gtab[1] = -4.0
1944        (2...Path_maxcoeff).each do |i|
1945          @path_gtab[i] = -4.0 * @path_gtab[i - 1] - @path_gtab[i - 2]
1946        end
1947        @path_gtab[m]
1948      end
1949      @path_ak_even = make_array(Path_maxcoeff - 1)
1950      (1...Path_maxcoeff).each do |m|
1951        @path_ak_even[m - 1] = make_array(m)
1952        (1..m).each do |k|
1953          @path_ak_even[m - 1][k - 1] = (-g.call(m - k) / g.call(m)).to_f
1954        end
1955      end
1956    end
1957
1958    def make_a_odd
1959      f = lambda do |m|
1960        @path_ftab = make_array(Path_maxcoeff) unless @path_ftab
1961        @path_ftab[0] = 1.0
1962        @path_ftab[1] = -3.0
1963        (2...Path_maxcoeff).each do |i|
1964          @path_ftab[i] = -4.0 * @path_ftab[i - 1] - @path_ftab[i - 2]
1965        end
1966        @path_ftab[m]
1967      end
1968      @path_ak_odd = make_array(Path_maxcoeff - 1)
1969      (1...Path_maxcoeff).each do |m|
1970        @path_ak_odd[m - 1] = make_array(m)
1971        (1..m).each do |k|
1972          @path_ak_odd[m - 1][k - 1] = (-f.call(m - k) / f.call(m)).to_f
1973        end
1974      end
1975    end
1976  end
1977
1978  class Open_bezier_path < Bezier_path
1979    def initialize(path, *args)
1980      super
1981      initial_direction, final_direction = nil
1982      optkey(args, binding,
1983             [:initial_direction, [0.0, 0.0, 0.0]],
1984             [:final_direction, [0.0, 0.0, 0.0]])
1985      @initial_direction = initial_direction
1986      @final_direction   = final_direction
1987    end
1988
1989    private
1990    def calculate_fit
1991      n = @x.length - 1
1992      m = n - 1
1993      p = [@x, @y, @z]
1994      d = make_array(3) do make_array(n + 1, 0.0) end
1995      d = Matrix[d[0], d[1], d[2]].to_a
1996      ref = lambda do |z, j, i|
1997        if i > n
1998          z[j][i - n]
1999        elsif i < 0
2000          z[j][i + n]
2001        elsif i == n
2002          z[j][n] - d[j][n]
2003        elsif i == 0
2004          z[j][0] + d[j][0]
2005        else
2006          z[j][i]
2007        end
2008      end
2009      d[0][0] = (@initial_direction[0] or 0.0)
2010      d[1][0] = (@initial_direction[1] or 0.0)
2011      d[2][0] = (@initial_direction[2] or 0.0)
2012      d[0][n] = (@final_direction[0] or 0.0)
2013      d[1][n] = (@final_direction[1] or 0.0)
2014      d[2][n] = (@final_direction[2] or 0.0)
2015      (1...n).each do |i|
2016        (1..[Path_maxcoeff - 1, m].min).each do |j|
2017          3.times do |k|
2018            d[k][i] = d[k][i] + ac(j, n) * (ref.call(p, k, i + j) - ref.call(p, k, i - j))
2019          end
2020        end
2021      end
2022      [n, p, d]
2023    end
2024
2025    def fit_path
2026      parse_path() if @x.nil?
2027      case points = @x.length
2028      when 1
2029        @bx = @by = @bz = nil
2030      when 2
2031        x1, x2 = @x[0, 2]
2032        y1, y2 = @y[0, 2]
2033        z1, z2 = @z[0, 2]
2034        @bx = [[x1, x1, x2, x2]]
2035        @by = [[y1, y1, y2, y2]]
2036        @bz = [[z1, z1, z2, z2]]
2037      else
2038        n, p, d = calculate_fit()
2039        c = @curvature
2040        cs = make_array(n)
2041        if c.kind_of?(NilClass) or (array?(c) and c.empty?)
2042          n.times do |i| cs[i] = [1.0, 1.0] end
2043        elsif number?(c)
2044          n.times do |i| cs[i] = [c, c] end
2045        elsif array?(c) and c.length == n
2046          c.each_with_index do |ci, i|
2047            cs[i] = if array?(ci)
2048                      if ci.length != 2
2049                        dl_error(ci, "curvature sublist must have two elements")
2050                      else
2051                        ci
2052                      end
2053                    else
2054                      [ci, ci]
2055                    end
2056          end
2057        else
2058          dl_error(c, "bad curvature argument to path, need #{n} elements")
2059        end
2060        @bx = (0...n).map do |i|
2061          [p[0][i], p[0][i] + d[0][i] * cs[i][0], p[0][i + 1] - d[0][i + 1] * cs[i][1], p[0][i + 1]]
2062        end
2063        @by = (0...n).map do |i|
2064          [p[1][i], p[1][i] + d[1][i] * cs[i][0], p[1][i + 1] - d[1][i + 1] * cs[i][1], p[1][i + 1]]
2065        end
2066        @bz = (0...n).map do |i|
2067          [p[2][i], p[2][i] + d[2][i] * cs[i][0], p[2][i + 1] - d[2][i + 1] * cs[i][1], p[2][i + 1]]
2068        end
2069      end
2070      reset_rendering()
2071    end
2072  end
2073
2074  class Closed_bezier_path < Bezier_path
2075    def initialize(path, *args)
2076      super
2077    end
2078
2079    private
2080    def calculate_fit
2081      n = @x.length - 1
2082      m = (n - (n.odd? ? 3 : 4)) / 2
2083      p = [@x, @y, @z]
2084      d = make_array(3) do make_array(n, 0.0) end
2085      ref = lambda do |z, j, i|
2086        if i > (n - 1)
2087          z[j][i - n]
2088        elsif i < 0
2089          z[j][i + n]
2090        else
2091          z[j][i]
2092        end
2093      end
2094      n.times do |i|
2095        (1..m).each do |j|
2096          3.times do |k|
2097            d[k][i] = d[k][i] + a(j, n) * (ref.call(p, k, i + j) - ref.call(p, k, i - j))
2098          end
2099        end
2100      end
2101      if @curvature
2102        n.times do |i|
2103          curve = @curvature[i]
2104          d[0][i] *= curve
2105          d[1][i] *= curve
2106          d[2][i] *= curve
2107        end
2108      end
2109      [n - 1, p, d]
2110    end
2111
2112    def fit_path
2113      parse_path() if @x.nil?
2114      if @x.length > 4
2115        n, p, d = calculate_fit()
2116        xc = (0...n).map do |i|
2117          [p[0][i], p[0][i] + d[0][i], p[0][i + 1] - d[0][i + 1], p[0][i + 1]]
2118        end
2119        yc = (0...n).map do |i|
2120          [p[1][i], p[1][i] + d[1][i], p[1][i + 1] - d[1][i + 1], p[1][i + 1]]
2121        end
2122        zc = (0...n).map do |i|
2123          [p[2][i], p[2][i] + d[2][i], p[2][i + 1] - d[2][i + 1], p[2][i + 1]]
2124        end
2125        @bx = xc + [[p[0][n], p[0][n] + d[0][n], p[0][0] - d[0][0], p[0][0]]]
2126        @by = yc + [[p[1][n], p[1][n] + d[1][n], p[1][0] - d[1][0], p[1][0]]]
2127        @bz = zc + [[p[2][n], p[2][n] + d[2][n], p[2][0] - d[2][0], p[2][0]]]
2128      else
2129        xc = []
2130        yc = []
2131        zc = []
2132        (@x.length - 1).times do |i|
2133          x1, x2 = @x[i, 2]
2134          y1, y2 = @y[i, 2]
2135          z1, z2 = @z[i, 2]
2136          xc << [x1, x1, x2, x2]
2137          yc << [y1, y1, y2, y2]
2138          zc << [z1, z1, z2, z2]
2139        end
2140        @bx = xc
2141        @by = yc
2142        @bz = zc
2143      end
2144      reset_rendering()
2145    end
2146  end
2147
2148  class Literal_path < Path
2149    def initialize(path, *args)
2150      @path  = path
2151      if (not @path) or (array?(@path) and @path.empty?)
2152        dl_error("can't define a path with no points in it")
2153      end
2154      super()
2155      d3, polar = nil
2156      optkey(args, binding,
2157             [:d3, true],
2158             [:polar, false])
2159      @d3    = d3
2160      @polar = polar
2161    end
2162
2163    private
2164    def render_path
2165      if @polar
2166        @rx, @ry, @rz, @rv = parse_polar_coordinates(@path, @d3)
2167      else
2168        @rx, @ry, @rz, @rv = parse_cartesian_coordinates(@path, @d3)
2169      end
2170      if (not @rv[0]) or @rv[0].zero?
2171        @rv[0] = 1.0
2172        @rv[-1] = 1.0
2173      end
2174      if @rx.length == 1
2175        @rt = [0.0]
2176        return
2177      end
2178      rx = @rx
2179      ry = @ry
2180      rz = @rz
2181      rv = @rv
2182      xseg = [rx[0]]
2183      yseg = [ry[0]]
2184      zseg = [rz[0]]
2185      vi = rv[0]
2186      ti = 0.0
2187      times = [ti]
2188      (1...rx.length).each do |i|
2189        x = rx[i]
2190        y = ry[i]
2191        z = rz[i]
2192        v = rv[i]
2193        xseg << x
2194        yseg << y
2195        zseg << z
2196        if v
2197          sofar = 0.0
2198          dseg = (0...xseg.length - 1).map do |j|
2199            xsi, xsf = xseg[j, 2]
2200            ysi, ysf = yseg[j, 2]
2201            zsi, zsf = zseg[j, 2]
2202            sofar += distance(xsf - xsi, ysf - ysi, zsf - zsi)
2203          end
2204          df = dseg[-1]
2205          vf = v
2206          aa = ((vf - vi) * (vf + vi)) / (df * 4.0)
2207          tseg = dseg.map do |d|
2208            ti + (if vi and vi.nonzero? and vf == vi
2209                    d / vi
2210                  elsif aa.nonzero?
2211                    ((vi * vi + 4.0 * aa * d) ** 0.5 - vi) / (2.0 * aa)
2212                  else
2213                    0.0
2214                  end)
2215          end
2216          times += tseg
2217          xseg = [x]
2218          yseg = [y]
2219          zseg = [z]
2220          vi = v
2221          ti = tseg[-1]
2222        end
2223      end
2224      tf = times[-1]
2225      @rt = times.map do |tii| tii / tf end
2226      reset_transformation()
2227    end
2228  end
2229
2230  class Spiral_path < Literal_path
2231    def initialize(*args)
2232      # to fool Literal_path.new
2233      super([nil])
2234      start_angle, turns = nil
2235      optkey(args, binding,
2236             [:start_angle, 0],
2237             [:turns, 2])
2238      @start_angle = start_angle
2239      @turns       = turns
2240    end
2241
2242    private
2243    def render_path
2244      start = (@start_angle / @one_turn.to_f) * TWO_PI
2245      total = (@turns.zero? ? TWO_PI : (@turns * TWO_PI))
2246      step_angle = @one_turn / 100.0
2247      steps = (total / ((step_angle / @one_turn.to_f) * TWO_PI)).abs
2248      step = total / (steps.ceil * (step_angle < 0 ? -1 : 1))
2249      x = []
2250      y = []
2251      z = []
2252      (total / step).round.abs.times do
2253        xy = cis(start)
2254        x << (10.0 * xy.imag)
2255        y << (10.0 * xy.real)
2256        z << 0.0
2257        start += step
2258      end
2259      sofar = 0.0
2260      dp = (0...x.length - 1).map do |i|
2261        xi, xf = x[i, 2]
2262        yi, yf = y[i, 2]
2263        zi, zf = z[i, 2]
2264        sofar += distance(xf - xi, yf - yi, zf - zi)
2265      end
2266      td = 0.0
2267      times = (0...dp.length - 1).map do |i|
2268        di, df = dp[i, 2]
2269        td = td + (df - di) / 4.0
2270      end
2271      @rx = x
2272      @ry = y
2273      @rz = z
2274      tf = times[-1]
2275      @rt = times.map do |ti| ti / tf end
2276      reset_transformation()
2277    end
2278  end
2279
2280  module_function
2281  def make_dlocsig(start, dur, *args)
2282    dl = Dlocsig.new
2283    dl.make_dlocsig(start, dur, *args)
2284    dl
2285  end
2286
2287  def dlocsig(dl, dloc, input)
2288    dl.dlocsig(dloc, input)
2289  end
2290
2291  def make_path(path, *args)
2292    Open_bezier_path.new(path, *args)
2293  end
2294
2295  def make_polar_path(path, *args)
2296    Open_bezier_path.new(path, :polar, true, *args)
2297  end
2298
2299  def make_closed_path(path, *args)
2300    d3 = nil
2301    optkey(args, binding, [:d3, true])
2302    len = d3 ? 3 : 2
2303    unless path[0][0, len] == path[-1][0, len]
2304      path += [path[0]]
2305    end
2306    Closed_bezier_path.new(path, *args)
2307  end
2308
2309  def make_literal_path(path, *args)
2310    Literal_path.new(path, *args)
2311  end
2312
2313  def make_literal_polar_path(path, *args)
2314    Literal_path.new(path, :polar, true, *args)
2315  end
2316
2317  def make_spiral_path(*args)
2318    Spiral_path.new(*args)
2319  end
2320end
2321
2322# example functions (see clm-2/dlocsig/move-sound.ins and
2323# clm-2/dlocsig/dlocsig.html)
2324
2325class Instrument
2326  def sinewave(start, dur, freq, amp, path, amp_env = [0, 1, 1, 1], *dlocsig_args)
2327    os = make_oscil(:frequency, freq)
2328    en = make_env(:envelope, amp_env, :scaler, amp, :duration, dur)
2329    run_dlocsig(start, dur, :path, path, *dlocsig_args) do
2330      env(en) * oscil(os)
2331    end
2332  end
2333
2334  def move(start, file, path, *dlocsig_args)
2335    dl_error(path, "need a path") unless path.kind_of?(DL::Path)
2336    dur = ws_duration(file)
2337    chns = ws_channels(file)
2338    rds = make_array(chns) do |chn| make_ws_reader(file, :start, start, :channel, chn) end
2339    run_dlocsig(start, dur, :path, path, *dlocsig_args) do
2340      rds.map do |rd| ws_readin(rd) end.sum / chns
2341    end
2342  end
2343
2344  add_help(:move_sound,
2345           "move_sound(path, *args) do ... end
2346 sound_let-args:
2347     :channels,    1  # channels to move
2348 start time in output file:
2349     :startime,    0
2350 rest args: make_dlocsig")
2351  def move_sound(path, *args, &body)
2352    chns        = get_shift_args(args, :channels, 1)
2353    start       = get_shift_args(args, :startime, 0)
2354    sound_let([:channels, chns, body]) do |to_move|
2355      if @verbose
2356        Snd.display("%s: moving sound on %d channel%s", get_func_name, chns, (chns > 1 ? "s" : ""))
2357      end
2358      move(0, to_move, path, *args)
2359      rbm_mix(to_move, :output_frame, seconds2samples(start))
2360    end
2361  end
2362end
2363
2364class With_sound
2365  def run_dlocsig(start, dur, *dlocsig_args, &body)
2366    with_sound_info(get_func_name(2), start, dur)
2367    dl = DL.make_dlocsig(start, dur,
2368                         :clm, @clm,
2369                         :rbm_output, @ws_output,
2370                         :rbm_reverb, @ws_reverb,
2371                         :out_channels, @channels,
2372                         :rev_channels, @reverb_channels,
2373                         *dlocsig_args)
2374    dl.ws_dlocsig(&body)
2375  end
2376end
2377
2378# Dlocsig menu
2379#
2380# require 'snd-xm'
2381#
2382# make_snd_menu("Dlocsig") do
2383#   cascade("Dlocsig (Snd)") do
2384#     entry(Dlocsig_bezier, "Bezier path (Snd)", true)
2385#     entry(Dlocsig_spiral, "Spiral path (Snd)", true)
2386#   end
2387#   cascade("Dlocsig (CLM)") do
2388#     entry(Dlocsig_bezier, "Bezier path (CLM)", false)
2389#     entry(Dlocsig_spiral, "Spiral path (CLM)", false)
2390#   end
2391# end
2392
2393if provided? :snd_motif
2394  class Dlocsig_menu
2395    require "snd-xm"
2396    include Snd_XM
2397    require "xm-enved"
2398    include DL
2399
2400    def initialize(label, snd_p)
2401      @label = label
2402      @snd_p = snd_p
2403      @dialog = nil
2404      @out_chans = 4
2405      @rev_chans = 1
2406      @path = nil
2407      @sliders = []
2408      @init_out_chans = 4
2409      @output_power = @init_power = 1.5
2410      @reverb_power = @init_rev_power = 0.5
2411      @render_using = Amplitude_panning
2412    end
2413
2414    private
2415    def with_sound_target(*comment_args)
2416      if @render_using == B_format_ambisonics
2417        set_scale_value(@sliders[0].scale, @out_chans = 4)
2418      end
2419      comment_string = if string?($clm_comment) and !$clm_comment.empty?
2420                         format("%s; %s", $clm_comment, format(*comment_args))
2421                       else
2422                         format(*comment_args)
2423                       end
2424      snd_path, snd_name = File.split(file_name(selected_sound))
2425      snd_name = snd_name.split("moved-").last
2426      snd_to_move = format("%s/%s", snd_path, snd_name)
2427      snd_moved = format("%s/moved-%s", snd_path, snd_name)
2428      path = @path
2429      output_power = @output_power
2430      reverb_power = @reverb_power
2431      render_using = @render_using
2432      f = with_sound(:clm, (not @snd_p),
2433                     :output, snd_moved,
2434                     :channels, @out_chans,
2435                     :reverb_channels, @rev_chans,
2436                     :comment, comment_string,
2437                     :info, (not $clm_notehook),
2438                     :statistics, true,
2439                     :play, true) do
2440        move(0,
2441             snd_to_move,
2442             path,
2443             :output_power, output_power,
2444             :reverb_power, reverb_power,
2445             :render_using, render_using)
2446      end
2447      Snd.display(f.output.inspect)
2448    rescue
2449      Snd.warning("%s#%s: %s", self.class, get_func_name, comment_string)
2450    end
2451
2452    def add_with_sound_sliders(parent = @dialog.parent)
2453      @sliders << @dialog.add_slider("output channels",
2454                               2, @init_out_chans, 8, 1, :linear, parent) do |w, c, i|
2455        @out_chans =   get_scale_value(w, i).round
2456      end
2457      @sliders <<  @dialog.add_slider("output power",
2458                                0, @init_power, 10, 100, :linear, parent) do |w, c, i|
2459        @output_power = get_scale_value(w, i, 100.0)
2460      end
2461      @sliders << @dialog.add_slider("reverb power",
2462                                     0, @init_rev_power, 10, 100, :linear, parent) do |w, c, i|
2463        @reverb_power = get_scale_value(w, i, 100.0)
2464      end
2465    end
2466
2467    def reset_with_sound_sliders(reverb_p = true)
2468      set_scale_value(@sliders[0].scale, @out_chans = @init_out_chans)
2469      @output_power = @init_power
2470      set_scale_value(@sliders[1].scale, @output_power, 100.0)
2471      @reverb_power = @init_rev_power
2472      set_scale_value(@sliders[2].scale, @reverb_power, 100.0)
2473    end
2474
2475    def add_with_sound_targets
2476      @dialog.add_target([["amplitude panning", :amplitude, true],
2477                          ["b format ambisonics", :b_format, false],
2478                          ["decoded ambisonics", :decoded, false]]) do |val|
2479        @render_using = case val
2480                        when :amplitude
2481                          Amplitude_panning
2482                        when :b_format
2483                          set_scale_value(@sliders[0].scale, @out_chans = 4)
2484                          B_format_ambisonics
2485                        when :decoded
2486                          Decoded_ambisonics
2487                        end
2488      end
2489      @dialog.add_target([["no reverb", :no_reverb, false],
2490                          ["1 rev chan", :one_rev_chan, true],
2491                          ["4 rev chans", :four_rev_chans, false]]) do |val|
2492        case val
2493        when :no_reverb
2494          @rev_chans = 0
2495        when :one_rev_chan
2496          @rev_chans = 1
2497        when :four_rev_chans
2498          @rev_chans = 4
2499        end
2500      end
2501    end
2502
2503    def set_xm_enveds_hooks(*enveds)
2504      enveds.each do |e|
2505        e.before_enved_hook.reset_hook! # to prevent running $enved_hook
2506        e.before_enved_hook.add_hook!("dlocsig-hook") do |pos, x, y, reason|
2507          if reason == Enved_move_point
2508            if e.in_range?(x)
2509              old_x = e.point(pos).first
2510              e.stretch!(old_x, x)
2511              e.point(pos, :y, y)
2512            else
2513              false
2514            end
2515          else
2516            false
2517          end
2518        end
2519        e.after_enved_hook.add_hook!("dlocsig-hook") do |pos, reason| show_values end
2520      end
2521    end
2522
2523  # comment string
2524  def dlocsig_strings
2525    dlstr = ["", :amplitude_panning, :b_format_ambisonics, :decoded_ambisonics]
2526    format("%s, output_power: %1.2f, reverb_power: %1.2f",
2527           dlstr[@render_using],
2528           @output_power,
2529           @reverb_power)
2530  end
2531
2532    def help_cb
2533      help_dialog(@label,
2534                  "\
2535The current sound will be moved through the chosen path.  You can set \
2536the reverberator via the global with-sound-variable $clm_reverb \
2537(#{$clm_reverb.inspect}).  If you want four reverb channels, you \
2538may try freeverb from freeverb.rb.
2539
2540reverb    reverb-channels output-channels source
2541
2542jc_reverb        1               4        examp.rb
2543jl_reverb        1               2        clm-ins.rb
2544nrev             1               4        clm-ins.rb
2545freeverb         4               > 4      freeverb.rb
2546
2547Amplitude-panning: generates amplitude panning between adjacent speakers.
2548
2549B-format-ambisonics: generates a four channel first order b-format encoded soundfile.
2550
2551Decoded-ambisonics: the ambisonics encoded information is decoded to the number of selected speakers.
2552
2553Note: reverb on spiral path generates noise if turns is less than 2.6
2554
2555For detailed information see clm-2/dlocsig.html.",
2556                  ["{Libxm}: graphics module",
2557                   "{Ruby}: extension language",
2558                   "{Motif}: Motif extensions via Libxm",
2559                   "{dlocsig}: Fernando Lopez Lezcano's multichannel locator"])
2560    end
2561  end
2562
2563  class Dlocsig_bezier < Dlocsig_menu
2564    require "xm-enved"
2565
2566    def initialize(label, snd_p = false)
2567      super
2568      @target = :with_sound
2569      @which_path = :open_bezier_path
2570      @snd_path = [[-10.0, 10.0, 0.0, 1.0], [0.0, 5.0, 1.0, 1.0], [10.0, 10.0, 0.0, 1.0]]
2571      @trajectory = nil
2572      @z_value = nil
2573      @velocity = nil
2574      @label_list = []
2575    end
2576
2577    def inspect
2578      str = @snd_path.inspect
2579      new_str = ""
2580      [str.length, 20].min.times do |i| new_str << str[i] end
2581      new_str << "..." if str.length > 20
2582      format("%s (%s)", @label, new_str)
2583    end
2584
2585    def post_dialog
2586      unless @dialog.kind_of?(Dialog) and widget?(@dialog.dialog)
2587        init_traj = [0, 1, 0.5, 0.5, 1, 1]
2588        init_z_traj = [0, 0, 0.5, 0.1, 1, 0]
2589        init_vel = [0, 0.5, 1, 0.5]
2590        @dialog = make_dialog(@label,
2591                              :help_cb, lambda do |w, c, i|
2592                                help_cb()
2593                              end, :clear_cb, lambda do |w, c, i|
2594                                create_path
2595                                @path.pplot
2596                              end, :reset_cb, lambda do |w, c, i|
2597                                reset_with_sound_sliders
2598                                @trajectory.envelope = init_traj
2599                                @z_value.envelope = init_z_traj
2600                                @velocity.envelope = init_vel
2601                                show_values
2602                              end) do |w, c, i|
2603          create_path
2604          with_sound_target("%s: %s, path: %s", @which_path, dlocsig_strings, @snd_path.inspect)
2605        end
2606        frame_args = [RXmNshadowThickness, 4,
2607                      RXmNshadowType, RXmSHADOW_ETCHED_OUT,
2608                      RXmNbackground, basic_color,
2609                      RXmNheight, 170,
2610                      RXmNwidth, 400]
2611        pane = RXtCreateManagedWidget("pane", RxmPanedWindowWidgetClass, @dialog.parent,
2612                                      [RXmNsashHeight, 1, RXmNsashWidth, 1,
2613                                       RXmNorientation, RXmHORIZONTAL,
2614                                       RXmNbackground, basic_color])
2615        xepane = RXtCreateManagedWidget("xepane", RxmPanedWindowWidgetClass, pane,
2616                                        [RXmNsashHeight, 1, RXmNsashWidth, 1,
2617                                         RXmNorientation, RXmVERTICAL,
2618                                         RXmNbackground, basic_color])
2619        trfr = RXtCreateManagedWidget("trfr", RxmFrameWidgetClass, xepane, frame_args)
2620        zfr = RXtCreateManagedWidget("zfr", RxmFrameWidgetClass, xepane, frame_args)
2621        vefr = RXtCreateManagedWidget("vefr", RxmFrameWidgetClass, xepane, frame_args)
2622        vepane = RXtCreateManagedWidget("vpane", RxmPanedWindowWidgetClass, pane,
2623                                        [RXmNsashHeight, 1, RXmNsashWidth, 1,
2624                                         RXmNseparatorOn, true,
2625                                         RXmNorientation, RXmVERTICAL,
2626                                         RXmNbackground, basic_color])
2627        add_with_sound_sliders(vepane)
2628        rc = RXtCreateManagedWidget("form", RxmRowColumnWidgetClass, vepane,
2629                                    [RXmNorientation, RXmVERTICAL,
2630                                     RXmNalignment, RXmALIGNMENT_CENTER])
2631        @label_list = make_array(8) do |i|
2632          RXtCreateManagedWidget("W" * 30, RxmLabelWidgetClass, rc, [])
2633        end
2634        add_with_sound_targets
2635        @dialog.add_target([["open bezier", :open_bezier_path, true],
2636                            ["closed bezier", :closed_bezier_path, false],
2637                            ["literal", :literal_path, false]]) do |val|
2638          @which_path = val
2639          create_path
2640        end
2641        activate_dialog(@dialog.dialog)
2642        @trajectory = make_xenved("x, y", trfr,
2643                                  :envelope, init_traj,
2644                                  :axis_bounds, [0.0, 1.0, 0.0, 1.0],
2645                                  :axis_label, [-10.0, 10.0, 0.0, 10.0])
2646        @z_value = make_xenved("z", zfr,
2647                               :envelope, init_z_traj,
2648                               :axis_bounds, [0.0, 1.0, 0.0, 1.0],
2649                               :axis_label, [-10.0, 10.0, 0.0, 10.0])
2650        @velocity = make_xenved("velocity v", vefr,
2651                                :envelope, init_vel,
2652                                :axis_bounds, [0.0, 1.0, 0.05, 1.0],
2653                                :axis_label, [-10.0, 10.0, 0.0, 2.0])
2654        set_xm_enveds_hooks(@trajectory, @z_value, @velocity)
2655        @dialog.clear_string("Gnuplot")
2656        @dialog.doit_string((@snd_p ? "With_Snd" : "With_Sound"))
2657        show_values
2658      else
2659        activate_dialog(@dialog.dialog)
2660      end
2661    end
2662
2663    private
2664    def create_path
2665      test_path
2666      @path = case @which_path
2667              when :open_bezier_path
2668                make_path(@snd_path)
2669              when :closed_bezier_path
2670                make_closed_path(@snd_path)
2671              when :literal_path
2672                make_literal_path(@snd_path)
2673              end
2674    end
2675
2676    def test_path
2677      if @snd_path.length == 2
2678        Snd.display("%s#%s: path has only two points, one added", self.class, get_func_name)
2679        @snd_path.insert(1, [0.0, @snd_path[0][1] - 0.1, 0.0, @snd_path[0][3] + 0.1])
2680      end
2681      unless @snd_path.detect do |pnt| pnt[1].nonzero? end
2682        Snd.display("%s#%s: y-values are all zero, changed to mid-point 0.1",
2683                    self.class, get_func_name)
2684        @snd_path[@snd_path.length / 2][1] = 0.1
2685      end
2686    end
2687
2688    def points_to_path
2689      @snd_path = []
2690      @trajectory.each do |x, y|
2691        z = @z_value.interp(x)
2692        vel = @velocity.interp(x)
2693        @snd_path.push([x * 20.0 - 10.0, y * 10.0, z * 10.0, vel * 2.0])
2694      end
2695    end
2696
2697    def show_values
2698      points_to_path
2699      @label_list.each_with_index do |w, i|
2700        if i.zero?
2701          change_label(w, format("%6s %6s %6s %6s", "x", "y", "z", "v"))
2702        else
2703          x, y, z, v = @snd_path[i - 1]
2704          if x
2705            change_label(w, format("%s %s %s %s",
2706                                   to_f_str(x), to_f_str(y), to_f_str(z), to_f_str(v)))
2707          else
2708            change_label(w, "")
2709          end
2710        end
2711      end
2712    end
2713
2714    def to_f_str(val)
2715      "%6s" % ("% 3.1f" % val)
2716    end
2717  end
2718
2719  class Dlocsig_spiral < Dlocsig_menu
2720    def initialize(label, snd_p = false)
2721      super
2722      @start = 0
2723      @turns = 3.0
2724    end
2725
2726    def inspect
2727      format("%s (%d %1.1f)", @label, @start, @turns)
2728    end
2729
2730    def post_dialog
2731      unless @dialog.kind_of?(Dialog) and widget?(@dialog.dialog)
2732        sliders = []
2733        init_start = 0
2734        init_turns = 3.0
2735        @dialog = make_dialog(@label,
2736                              :help_cb, lambda do |w, c, i|
2737                                help_cb
2738                              end, :clear_cb, lambda do |w, c, i|
2739                                make_spiral_path(:start_angle, @start, :turns, @turns).pplot
2740                              end, :reset_cb, lambda do |w, c, i|
2741                                reset_with_sound_sliders
2742                                set_scale_value(sliders[0].scale, @start = init_start)
2743                                @turns = init_turns
2744                                set_scale_value(sliders[1].scale, init_turns, 10.0)
2745                              end) do |w, c, i|
2746          @path = make_spiral_path(:start_angle, @start, :turns, @turns)
2747          with_sound_target("spiral_path: %s, start: %d, turns: %1.1f",
2748                            dlocsig_strings, @start, @turns)
2749        end
2750        add_with_sound_sliders(if provided? :xg
2751                                 @dialog.dialog
2752                               else
2753                                 @dialog.parent
2754                               end)
2755        sliders << @dialog.add_slider("start angle", 0, init_start, 360) do |w, c, i|
2756          @start = get_scale_value(w, i)
2757        end
2758        # turns below 2.6 together with reverb create noise
2759        sliders << @dialog.add_slider("turns", 2.6, init_turns, 10.0, 10) do |w, c, i|
2760          @turns = get_scale_value(w, i, 10.0)
2761        end
2762        add_with_sound_targets
2763        @dialog.clear_string("Gnuplot")
2764        @dialog.doit_string((@snd_p ? "With_Snd" : "With_Sound"))
2765      end
2766      activate_dialog(@dialog.dialog)
2767    end
2768  end
2769
2770  unless defined? $__private_dlocsig_menu__ and $__private_dlocsig_menu__
2771    snd_main = make_snd_menu("Dlocsig") do
2772      cascade("Dlocsig (Snd)") do
2773        entry(Dlocsig_bezier, "Bezier path (Snd)", true)
2774        entry(Dlocsig_spiral, "Spiral path (Snd)", true)
2775      end
2776      cascade("Dlocsig (CLM)") do
2777        entry(Dlocsig_bezier, "Bezier path (CLM)", false)
2778        entry(Dlocsig_spiral, "Spiral path (CLM)", false)
2779      end
2780    end
2781
2782    if provided? :xm
2783      set_label_sensitive(menu_widgets[Top_menu_bar], "Dlocsig", ((sounds() or []).length > 1))
2784    else
2785      set_sensitive(snd_main.menu, ((sounds() or []).length > 1))
2786    end
2787
2788    unless $open_hook.member?("dlocsig-menu-hook")
2789      $open_hook.add_hook!("dlocsig-menu-hook") do |snd|
2790        if provided? :xm
2791          set_label_sensitive(menu_widgets[Top_menu_bar], "Dlocsig", true)
2792        else
2793          set_sensitive(snd_main.menu, true)
2794        end
2795        false
2796      end
2797
2798      $close_hook.add_hook!("dlocsig-menu-hook") do |snd|
2799        if provided? :xm
2800          set_label_sensitive(menu_widgets[Top_menu_bar], "Dlocsig", ((sounds() or []).length > 1))
2801        else
2802          set_sensitive(snd_main.menu, ((sounds() or []).length > 1))
2803        end
2804        false
2805      end
2806    end
2807  end
2808end
2809
2810# JC_REVERB (examp.rb) default options
2811#
2812# :low_pass, false
2813# :volume,   1.0
2814# :amp_env,  false
2815# :delay1,   0.013
2816# :delay2,   0.011
2817# :delay3,   0.015
2818# :delay4,   0.017
2819# :double,   false
2820#
2821# $clm_reverb = :jc_reverb_rb
2822# $clm_reverb_data = [:low_pass, false, :volume, 1.0, :amp_env, false,
2823#                    :delay1, 0.013, :delay2, 0.011, :delay3, 0.015, :delay4, 0.017]
2824
2825# require "clm-ins"
2826#
2827# JL_REVERB has no options
2828#
2829# $clm_reverb = :jl_reverb
2830# $clm_reverb_data = []
2831
2832# NREV default options
2833#
2834# :reverb_factor, 1.09
2835# :lp_coeff,      0.7
2836# :volume,        1.0
2837#
2838# $clm_reverb = :nrev_rb
2839# $clm_reverb_data = [:volume, 1.0, :lp_coeff, 0.7]
2840
2841# INTERN or N_REV default options (only with_snd)
2842#
2843# :amount,   0.1
2844# :filter,   0.5
2845# :feedback, 1.09
2846#
2847# $clm_reverb = :intern
2848# $clm_reverb_data = [:amount, 0.1, :filter, 0.5, :feedback, 1.09]
2849
2850# require "freeverb"
2851#
2852# FREEVERB default options
2853#
2854# :room_decay,        0.5,
2855# :damping,           0.5,
2856# :global,            0.3,
2857# :predelay,          0.03,
2858# :output_gain,       1.0,
2859# :output_mixer,      nil,
2860# :scale_room_decay,  0.28,
2861# :offset_room_decay, 0.7,
2862# :combtuning,        [1116, 1188, 1277, 1356, 1422, 1491, 1557, 1617],
2863# :allpasstuning,     [556, 441, 341, 225],
2864# :scale_damping,     0.4,
2865# :stereo_spread,     23,
2866#
2867# $clm_reverb = :freeverb
2868# $clm_reverb_data = [:room_decay, 0.5, :damping, 0.5, :global, 0.3, :predelay, 0.03,
2869#                     :output_gain, 1.0, :output_mixer, nil, :scale_room_decay, 0.7,
2870#                     :scale_damping, 0.4, :stereo_spread, 23]
2871
2872=begin
2873# (snd-ruby-mode)
2874# Examples:
2875
2876(with_sound(:channels, 4, :output, "rdloc04.snd") do
2877   sinewave(0, 1, 440, 0.5, [[-10, 10], [0, 5], [10, 10]].to_path)
2878 end)
2879
2880(with_sound(:channels, 4, :output, "rdlocspiral.snd") do
2881   move(0, "/usr/gnu/sound/SFiles/bell.snd", DL.make_spiral_path(:start_angle, 180, :turns, 3.5))
2882 end)
2883
2884([[-10, 10, 0, 0], [0, 5, 0, 1], [10, 10, 0, 0]].to_path(:error, 0.01).plot_velocity)
2885
2886(with_sound(:channels, 4, :output, "rdlocmove.snd") do
2887   move_sound(DL.make_path([[-10, 10], [0.1, 0.1], [10, -10]])) do
2888     fm_violin_rb(0, 1, 440, 0.1)
2889     fm_violin_rb(0.3, 2, 1020, 0.05)
2890   end
2891 end)
2892=end
2893
2894# dlocsig.rb ends here
2895