1## Copyright (C) 2019-2021 John Donoghue <john.donoghue@ieee.org>
2##
3## This program is free software: you can redistribute it and/or modify it
4## under the terms of the GNU General Public License as published by
5## the Free Software Foundation, either version 3 of the License, or
6## (at your option) any later version.
7##
8## This program is distributed in the hope that it will be useful, but
9## WITHOUT ANY WARRANTY; without even the implied warranty of
10## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11## GNU General Public License for more details.
12##
13## You should have received a copy of the GNU General Public License
14## along with this program.  If not, see
15## <https://www.gnu.org/licenses/>.
16
17classdef midicontrols < handle
18  ## -*- texinfo -*-
19  ## @deftypefn {} {@var{obj} =} midicontrols ()
20  ## @deftypefnx {} {@var{obj} =} midicontrols (@var{ctrlid})
21  ## @deftypefnx {} {@var{obj} =} midicontrols (@var{ctrlid}, @var{initialvalues})
22  ## @deftypefnx {} {@var{obj} =} midicontrols (__, @var{propertyname}, @var{propertyvalue})
23  ## Create a midi controls object
24  ##
25  ## @subsubheading Inputs
26  ## @var{ctrlid} - single control id or array of control ids to monitor, or [] to use any controller.@*
27  ## @var{initialvalues} - initial values to use for controls. It should be the same size as @var{ctrlid}@*
28  ## @var{propertyname}, @var{propertyvalue} - properties to set on the controller. If a device is not specified
29  ## the value from getpref("midi", "DefaultDevice", 0) will be used.@*
30  ##
31  ##
32  ## Known properties are:
33  ## @table @asis
34  ## @item mididevice
35  ## name of the mididevice to monitor.
36  ## @item outputmode
37  ## the scaling mode for values: 'rawmidi' will return values between 0 .. 127,
38  ## 'normalized' will use values between 0 .. 1.
39  ## @end table
40  ##
41  ## @subsubheading Outputs
42  ## @var{obj} - returns a midicontrols object
43  ##
44  ## @subsubheading Examples
45  ## Create a midicontrols object monitoring control id 2001 on the default midi device
46  ## @example
47  ## @code {
48  ## ctrl = midicontrols(2001)
49  ## }
50  ## @end example
51  ##
52  ## Create a midicontrols object monitoring control id 2001 on a a non default device
53  ## @example
54  ## @code {
55  ## ctrl = midicontrols(2001, 'mididevice', 1)
56  ## }
57  ## @end example
58  ##
59  ## @seealso{midiread, midisync}
60  ## @end deftypefn
61
62  properties (Access = private)
63   controls = [];
64   initialvalue = 0;
65   currentvalue = [];
66   device = [];
67   outscale = 1;
68   midiscale = 127;
69  endproperties
70
71  methods
72
73    function this = midicontrols (varargin)
74      devicename = "";
75
76      if nargin > 0
77        controls = varargin{1};
78        if !isnumeric(controls)
79          error ("Expected numeric controls ids");
80        endif
81        if isscalar(controls)
82          this.controls = [controls];
83        else
84          this.controls = controls;
85        endif
86      endif
87
88      if nargin > 1
89        if ischar (varargin{2})
90          propstart = 2;
91          initvals = [0];
92        else
93          propstart = 3;
94          initvals = varargin{2};
95
96          if !isnumeric (initvals)
97            error ("Expected numeric initial values");
98          endif
99          if isscalar (initvals)
100            initvals = [initvals];
101          endif
102        endif
103
104        if mod (nargin-propstart + 1, 2) != 0
105          error ("midicontrols: expected property name, value pairs");
106        endif
107        if !iscellstr (varargin (propstart:2:nargin))
108          error ("midicontrols: expected property names to be strings");
109        endif
110
111        for i = propstart:2:nargin
112          propname = tolower (varargin{i});
113          propvalue = varargin{i+1};
114
115          if strcmp (propname, "outputmode")
116            if !ischar (propvalue)
117              error ("output mode should be 'normalized' or 'rawmidi'")
118            elseif strcmpi (propvalue, "normalized")
119              this.outscale = 1;
120            elseif strcmpi (propvalue, "rawmidi")
121              this.outscale = 127;
122            else
123              error ("output mode should be 'normalized' or 'rawmidi'")
124            endif
125          elseif strcmp (propname, "mididevice")
126            devicename = propvalue;
127          else
128            error ("unknown property '%s'", propname)
129          endif
130        endfor
131
132        this.initialvalue = initvals/this.outscale;
133      endif
134
135      if length (this.controls) > 0
136        this.currentvalue = zeros (length(this.controls), 1);
137      else
138        this.currentvalue = zeros (1, 1);
139      endif
140
141      for i = 1:length (this.currentvalue)
142        this.currentvalue(i) = this.get_value (i, this.initialvalue);
143      endfor
144
145      if isempty(devicename)
146        devicename = getpref ("midi", "DefaultDevice", 0);
147      endif
148      this.device = mididevice (devicename);
149    endfunction
150
151    function send (this, values)
152      if nargin < 2
153        values = this.initialvalue;
154      else
155        if isscalar(values)
156          values = [values];
157        endif
158        values = values/this.outscale;
159      endif
160
161      if isempty(this.controls)
162        warning ('Can not send control values when no specific controller ids were provided.')
163        val = this.get_value(1, values);
164        this.currentvalue(1) = val;
165      else
166        for i =1:length(this.controls)
167          ctrl = this.controls(i);
168          ch = int32(ctrl/1000);
169          id = mod(ctrl, 1000);
170
171          val = this.get_value(i, values);
172          this.currentvalue(i) = val;
173
174          midisend(this.device, midimsg("controlchange", ch, id, val*this.midiscale));
175        endfor
176      endif
177
178    endfunction
179
180    function val = recv(this)
181      mx = midireceive(this.device);
182      while !isempty(mx)
183        for j = 1:length(mx)
184          m = mx(j);
185          if strcmp(m.type, "ControlChange")
186            if isempty(this.controls)
187              idx = 1;
188            else
189              ctrlid = m.channel*1000 + double(m.msgbytes(2));
190              idx = find(this.controls==ctrlid, 1);
191            endif
192            if !isempty(idx)
193              val = double(m.msgbytes(3))/this.midiscale;
194              this.currentvalue(idx) = val;
195            endif
196          endif
197        endfor
198        mx = midireceive(this.device);
199      endwhile
200
201      val = zeros(length(this.currentvalue));
202      for i = 1:length(this.currentvalue)
203        val(i) = this.get_value(i, this.currentvalue)*this.outscale;
204      endfor
205    endfunction
206
207    function val = get_value(this, ch, values)
208      if isscalar(values)
209        val = values;
210      else
211        if i > length(values)
212          val = values(i);
213        else
214          val = 0;
215        endif
216      endif
217      val = double(val);
218    endfunction
219
220    function out = disp (this)
221      if nargout == 0
222        disp(sprintf ("  midicontrols object: listening for events on %s",  this.device.Input));
223      else
224        out = sprintf ("midicontrols object: listening for events on %s\n",  this.device.Input);
225      endif
226      if ( isempty(this.controls))
227        if nargout == 0
228          disp ("    any control"); % any control on {devname}
229        else
230          out = [out "  any control\n"];
231        endif
232      else
233        if nargout == 0
234          disp (["    controls " sprintf("%d ", this.controls)]);
235        else
236          out = [out "  controls " sprintf("%d ", this.controls) "\n"];
237        endif
238      endif
239    endfunction
240  endmethods
241endclassdef
242