[Qt4Mixer/MAudio] add mixer GUI for a part of M-Audio Firewire series
[ffado.git] / libffado / support / mixer-qt4 / ffado / mixer / maudio_bebob.py
blobc30dd2f62ad3edf6173f6f2bb1633aebcad9de50
1 from PyQt4.QtCore import SIGNAL, SLOT, QObject, Qt
2 from PyQt4.QtGui import QWidget, QMessageBox, QHBoxLayout, QVBoxLayout, QTabWidget, QGroupBox, QLabel, QDial, QSlider, QToolButton, QSizePolicy
3 from math import log10
4 from ffado.config import *
6 import logging
7 log = logging.getLogger('MAudioBeBoB')
9 class MAudio_BeBoB_Input_Widget(QWidget):
10 def __init__(self,parent = None):
11 QWidget.__init__(self,parent)
12 uicLoad("ffado/mixer/maudio_bebob_input", self)
14 class MAudio_BeBoB_Output_Widget(QWidget):
15 def __init__(self,parent = None):
16 QWidget.__init__(self,parent)
17 uicLoad("ffado/mixer/maudio_bebob_output", self)
20 class MAudio_BeBoB(QWidget):
21 def __init__(self,parent = None):
22 QWidget.__init__(self,parent)
24 info = {0x0000000a: [0, "Ozonic"],
25 0x00010062: [1, "Firewire Solo"],
26 0x00010060: [2, "Firewire Audiophile"],
27 0x00010046: [3, "Firewire 410"],
30 labels = [
31 {"inputs": ["Analog 1/2", "Analog 3/4", "Stream 1/2", "Stream 3/4"],
32 "mixers": ["Mixer 1/2", "Mixer 3/4"],
33 "outputs": ["Analog 1/2", "Analog 3/4"]},
34 {"inputs": ["Analog 1/2", "Digital 1/2", "Stream 1/2", "Stream 3/4"],
35 "mixers": ["Mixer 1/2", "Mixer 3/4"],
36 "outputs": ["Analog 1/2", "Digital 1/2"]},
37 {"inputs": ["Analog 1/2", "Digital 1/2", "Stream 1/2", "Stream 3/4", "Stream 5/6"],
38 "mixers": ["Mixer 1/2", "Mixer 3/4", "Mixer 5/6", "Aux 1/2"],
39 "outputs": ["Analog 1/2", "Analog 3/4", "Digital 1/2", "Headphone 1/2"]},
40 {"inputs": ["Analog 1/2", "Digital 1/2", "Stream 1/2", "Stream 3/4", "Stream 5/6", "Stream 7/8", "Stream 9/10"],
41 "mixers": ["Mixer 1/2", "Mixer 3/4", "Mixer 5/6", "Mixer 7/8", "Mixer 9/10", "Aux 1/2"],
42 "outputs": ["Analog 1/2", "Analog 3/4", "Analog 5/6", "Analog 7/8", "Digital 1/2", "Headphone 1/2"]}
45 # hardware inputs and stream playbacks
46 # format: function_id/channel_idx/panning-able
47 # NOTE: function_id = channel_idx = panning-able = labels["inputs"]
48 inputs = [
49 [[0x03, 0x04, 0x01, 0x02],
50 [[0x01, 0x02], [0x01, 0x02], [0x01, 0x02], [0x01, 0x02]],
51 [True, True, False, False]],
52 [[0x01, 0x02, 0x04, 0x03],
53 [[0x01, 0x02], [0x01, 0x02], [0x01, 0x02], [0x01, 0x02]],
54 [True, True, False, False]],
55 [[0x04, 0x05, 0x01, 0x02, 0x03],
56 [[0x01, 0x02], [0x01, 0x02], [0x01, 0x02], [0x01, 0x02], [0x01, 0x02]],
57 [True, True, False, False, False]],
58 [[0x03, 0x04, 0x02, 0x01, 0x01, 0x01, 0x01],
59 [[0x01, 0x02], [0x01, 0x02], [0x01, 0x02], [0x01, 0x02], [0x03, 0x04], [0x05, 0x06], [0x07, 0x08]],
60 [True, True, False, False, False, False, False]]
63 # jack sources except for headphone
64 # format: function_id/source id
65 # NOTE: "function_id" = labels["output"] - "Headphone 1/2/3/4"
66 # NOTE: "source_id" = labels["mixer"]
67 jack_src = [
68 None,
69 None,
70 [[0x01, 0x02, 0x03],
71 [0x00, 0x00, 0x00, 0x01]],
72 [[0x02, 0x03, 0x04, 0x05, 0x06],
73 [0x00, 0x00, 0x00, 0x00, 0x00, 0x01]]
76 # headphone sources
77 # format: sink id/source id
78 # NOTE: "source_id" = labels["mixer"]
79 hp_src = [
80 None,
81 None,
82 [[0x04],
83 [0x00, 0x01, 0x02, 0x03]],
84 [[0x07],
85 [0x02, 0x03, 0x04, 0x05, 0x06, 0x07]]
88 # hardware outputs
89 # format: function id
90 # NOTE: "function_id" = labels["output"]
91 outputs = [
92 [0x05, 0x06],
93 [0x02, 0x03],
94 [0x0c, 0x0d, 0x0e, 0x0f],
95 [0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f]
98 # Mixer inputs/outputs
99 # format: function_id/output_stereo_channel_id/input_id/input_stereo_channel_id
100 # NOTE: function_id = output_stereo_channel_id = labels["mixers"]
101 # NOTE: input_id = input_stereo_channel_id = labels["inputs"]
102 mixers = [
103 [[0x01, 0x02],
104 [[0x01, 0x02], [0x01, 0x02]],
105 [0x02, 0x03, 0x00, 0x01],
106 [[0x01, 0x02], [0x01, 0x02], [0x01, 0x02], [0x01, 0x02]]],
107 [[0x01, 0x01],
108 [[0x01, 0x02], [0x03, 0x04]],
109 [0x00, 0x01, 0x03, 0x02],
110 [[0x01, 0x02], [0x01, 0x02], [0x01, 0x02], [0x01, 0x02]]],
111 [[0x01, 0x02, 0x03, 0x04],
112 [[0x01, 0x02], [0x01, 0x02], [0x01, 0x02], [0x01, 0x02]],
113 [0x03, 0x04, 0x00, 0x01, 0x02],
114 [[0x01, 0x02], [0x01, 0x02], [0x01, 0x02], [0x01, 0x02], [0x01, 0x02]]],
115 [[0x01, 0x01, 0x01, 0x01, 0x01, 0x07],
116 [[0x01, 0x02], [0x03, 0x04], [0x05, 0x06], [0x07, 0x08], [0x09, 0x0a], [0x01, 0x02]],
117 [0x02, 0x03, 0x01, 0x00, 0x00, 0x00, 0x00],
118 [[0x01, 0x02], [0x01, 0x02], [0x01, 0x02], [0x01, 0x02], [0x03, 0x04], [0x05, 0x06], [0x07, 0x08]]]
121 # Aux mixer inputs/outputs
122 # format: function_id/input_id/input_stereo_channel_id
123 # NOTE: input_id = labels["inputs"]
124 aux = [
125 None,
126 None,
127 [0x0b,
128 [0x09, 0x0a, 0x06, 0x07, 0x08],
129 [[0x01, 0x02], [0x01, 0x02], [0x01, 0x02], [0x01, 0x02], [0x01, 0x02]]],
130 [0x09,
131 [0x07, 0x08, 0x06, 0x05, 0x05, 0x05, 0x05],
132 [[0x01, 0x02], [0x01, 0x02], [0x01, 0x02], [0x01, 0x02], [0x03, 0x04], [0x05, 0x06], [0x07, 0x08]]]
135 def getDisplayTitle(self):
136 model = self.configrom.getModelId()
137 return self.info[model][1]
139 def buildMixer(self):
140 self.Selectors = {}
141 self.Pannings = {}
142 self.Volumes = {}
143 self.Mutes = {}
144 self.Mixers = {}
145 self.FW410HP = 0
147 model = self.configrom.getModelId()
148 if model not in self.info:
149 return;
151 self.id = self.info[model][0]
152 name = self.info[model][1]
154 tabs_layout = QHBoxLayout(self)
155 tabs = QTabWidget(self)
157 self.addInputTab(tabs)
158 self.addMixTab(tabs)
159 if self.aux[self.id] is not None:
160 self.addAuxTab(tabs)
161 self.addOutputTab(tabs)
163 tabs_layout.addWidget(tabs)
166 def addInputTab(self, tabs):
167 tab_input = QWidget(self)
168 tabs.addTab(tab_input, "In")
170 tab_input_layout = QHBoxLayout()
171 tab_input.setLayout(tab_input_layout)
173 in_labels = self.labels[self.id]["inputs"]
174 in_ids = self.inputs[self.id][0]
175 in_ch_ids = self.inputs[self.id][1]
176 in_pan = self.inputs[self.id][2]
178 for i in range(len(in_ids)):
179 l_idx = self.inputs[self.id][1][i][0]
180 r_idx = self.inputs[self.id][1][i][1]
182 widget = MAudio_BeBoB_Input_Widget(tab_input)
183 tab_input_layout.addWidget(widget)
185 widget.name.setText(in_labels[i])
187 self.Volumes[widget.l_sld] = ["/Mixer/Feature_Volume_%d" % in_ids[i], l_idx, widget.r_sld, r_idx, widget.link]
188 self.Volumes[widget.r_sld] = ["/Mixer/Feature_Volume_%d" % in_ids[i], r_idx, widget.l_sld, l_idx, widget.link]
189 self.Mutes[widget.mute] = [widget.l_sld, widget.r_sld]
191 if not in_pan[i]:
192 widget.l_pan.setDisabled(True)
193 widget.r_pan.setDisabled(True)
194 else:
195 self.Pannings[widget.l_pan] = ["/Mixer/Feature_LRBalance_%d" % in_ids[i], l_idx]
196 self.Pannings[widget.r_pan] = ["/Mixer/Feature_LRBalance_%d" % in_ids[i], r_idx]
198 tab_input_layout.addStretch()
201 def addMixTab(self, tabs):
202 tab_mix = QWidget(self)
203 tabs.addTab(tab_mix, "Mix")
205 tab_layout = QHBoxLayout()
206 tab_mix.setLayout(tab_layout)
208 in_labels = self.labels[self.id]["inputs"]
209 in_idxs = self.inputs[self.id][0]
211 mix_labels = self.labels[self.id]["mixers"]
212 mix_idxs = self.mixers[self.id][0]
214 for i in range(len(mix_idxs)):
215 if mix_labels[i] == 'Aux 1/2':
216 continue
218 grp = QGroupBox(tab_mix)
219 grp_layout = QVBoxLayout()
220 grp.setLayout(grp_layout)
221 tab_layout.addWidget(grp)
223 label = QLabel(grp)
224 grp_layout.addWidget(label)
226 label.setText(mix_labels[i])
227 label.setAlignment(Qt.AlignCenter)
228 label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
230 for j in range(len(in_idxs)):
231 mix_in_id = self.mixers[self.id][2][j]
233 button = QToolButton(grp)
234 grp_layout.addWidget(button)
236 button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
237 button.setText('%s In' % in_labels[j])
238 button.setCheckable(True)
240 self.Mixers[button] = ["/Mixer/EnhancedMixer_%d" % mix_idxs[i], mix_in_id, j, i]
242 grp_layout.addStretch()
243 tab_layout.addStretch()
245 def addAuxTab(self, tabs):
246 #local functions
247 def addLinkButton(parent, layout):
248 button = QToolButton(grp)
249 grp_layout.addWidget(button)
250 button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
251 button.setText('Link')
252 button.setCheckable(True)
253 return button
254 def addMuteButton(parent, layout):
255 button = QToolButton(parent)
256 layout.addWidget(button)
257 button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
258 button.setText('Mute')
259 button.setCheckable(True)
260 return button
262 # local processing
263 tab_aux = QWidget(self)
264 tabs.addTab(tab_aux, "Aux")
266 layout = QHBoxLayout()
267 tab_aux.setLayout(layout)
269 aux_label = self.labels[self.id]["mixers"][-1]
270 aux_id = self.aux[self.id][0]
272 aux_in_labels = self.labels[self.id]["inputs"]
273 aux_in_ids = self.aux[self.id][1]
275 for i in range(len(aux_in_ids)):
276 in_ch_l = self.aux[self.id][2][i][0]
277 in_ch_r = self.aux[self.id][2][i][1]
279 grp = QGroupBox(tab_aux)
280 grp_layout = QVBoxLayout()
281 grp.setLayout(grp_layout)
282 layout.addWidget(grp)
284 label = QLabel(grp)
285 grp_layout.addWidget(label)
286 label.setText("%s\nIn" % aux_in_labels[i])
287 label.setAlignment(Qt.AlignCenter)
289 grp_sld = QGroupBox(grp)
290 grp_sld_layout = QHBoxLayout()
291 grp_sld.setLayout(grp_sld_layout)
292 grp_layout.addWidget(grp_sld)
294 l_sld = QSlider(grp_sld)
295 grp_sld_layout.addWidget(l_sld)
296 r_sld = QSlider(grp_sld)
297 grp_sld_layout.addWidget(r_sld)
299 button = addLinkButton(grp, grp_layout)
300 self.Volumes[l_sld] = ["/Mixer/Feature_Volume_%d" % aux_in_ids[i], in_ch_l, r_sld, in_ch_r, button]
301 self.Volumes[r_sld] = ["/Mixer/Feature_Volume_%d" % aux_in_ids[i], in_ch_r, l_sld, in_ch_l, button]
303 button = addMuteButton(grp, grp_layout)
304 self.Mutes[button] = [l_sld, r_sld]
306 grp = QGroupBox(tab_aux)
307 grp_layout = QVBoxLayout()
308 grp.setLayout(grp_layout)
309 layout.addWidget(grp)
311 label = QLabel(grp)
312 grp_layout.addWidget(label)
313 label.setText("%s\nOut" % aux_label)
314 label.setAlignment(Qt.AlignCenter)
316 grp_sld = QGroupBox(grp)
317 grp_sld_layout = QHBoxLayout()
318 grp_sld.setLayout(grp_sld_layout)
319 grp_layout.addWidget(grp_sld)
321 l_sld = QSlider(grp_sld)
322 grp_sld_layout.addWidget(l_sld)
323 r_sld = QSlider(grp_sld)
324 grp_sld_layout.addWidget(r_sld)
326 button = addLinkButton(grp, grp_layout)
327 self.Volumes[l_sld] = ["/Mixer/Feature_Volume_%d" % aux_id, 1, r_sld, 2, button]
328 self.Volumes[r_sld] = ["/Mixer/Feature_Volume_%d" % aux_id, 2, l_sld, 1, button]
330 button = addMuteButton(grp, grp_layout)
331 self.Mutes[button] = [l_sld, r_sld]
333 layout.addStretch()
335 def addOutputTab(self, tabs):
336 tab_out = QWidget(self)
337 tabs.addTab(tab_out, "Out")
339 layout = QHBoxLayout()
340 tab_out.setLayout(layout)
342 out_labels = self.labels[self.id]["outputs"]
343 out_ids = self.outputs[self.id]
345 mixer_labels = self.labels[self.id]["mixers"]
347 if self.jack_src[self.id] is None:
348 for i in range(len(out_ids)):
349 label = QLabel(tab_out)
350 layout.addWidget(label)
351 label.setText("%s Out is fixed to %s Out" % (mixer_labels[i], out_labels[i]))
352 return
354 mixer_ids = self.jack_src[self.id][1]
357 for i in range(len(out_ids)):
358 out_label = out_labels[i]
359 if out_label.find('Headphone') >= 0:
360 continue
362 out_id = self.jack_src[self.id][0][i]
364 widget = MAudio_BeBoB_Output_Widget(tab_out)
365 layout.addWidget(widget)
367 widget.name.setText(out_label)
369 self.Volumes[widget.l_sld] = ["/Mixer/Feature_Volume_%d" % out_ids[i], 1, widget.r_sld, 2, widget.link]
370 self.Volumes[widget.r_sld] = ["/Mixer/Feature_Volume_%d" % out_ids[i], 2, widget.l_sld, 1, widget.link]
371 self.Mutes[widget.mute] = [widget.l_sld, widget.r_sld]
373 self.Selectors[widget.cmb_src] = ["/Mixer/Selector_%d" % out_id]
375 for j in range(len(mixer_ids)):
376 if (i != j and j != len(mixer_ids) - 1):
377 continue;
378 widget.cmb_src.addItem("%s Out" % mixer_labels[j], mixer_ids[j])
380 # add headphone
381 for i in range(len(out_ids)):
382 out_label = out_labels[i]
383 if out_label.find('Headphone') < 0:
384 continue
386 hp_label = self.labels[self.id]["outputs"][i]
387 hp_id = self.hp_src[self.id][0][0]
389 mixer_labels = self.labels[self.id]["mixers"]
391 widget = MAudio_BeBoB_Output_Widget(tab_out)
392 layout.addWidget(widget)
394 widget.name.setText(hp_label)
396 mixer_labels = self.labels[self.id]["mixers"]
397 mixer_ids = self.mixers[self.id][0]
399 self.Volumes[widget.l_sld] = ["/Mixer/Feature_Volume_%d" % out_ids[i], 1, widget.r_sld, 2, widget.link]
400 self.Volumes[widget.r_sld] = ["/Mixer/Feature_Volume_%d" % out_ids[i], 2, widget.l_sld, 1, widget.link]
401 self.Mutes[widget.mute] = [widget.l_sld, widget.r_sld]
403 for i in range(len(mixer_ids)):
404 widget.cmb_src.addItem("%s Out" % mixer_labels[i], mixer_ids[i])
406 if self.id != 3:
407 self.Selectors[widget.cmb_src] = ["/Mixer/Selector_%d" % hp_id]
408 else:
409 QObject.connect(widget.cmb_src, SIGNAL('activated(int)'), self.update410HP)
410 self.FW410HP = widget.cmb_src
412 layout.addStretch()
414 def initValues(self):
415 for ctl, params in self.Selectors.items():
416 path = params[0]
417 state = self.hw.getDiscrete(path)
418 ctl.setCurrentIndex(state)
419 QObject.connect(ctl, SIGNAL('activated(int)'), self.updateSelector)
421 # Right - Center - Left
422 # 0x8000 - 0x0000 - 0x0001 - 0x7FFE
423 # ..., -1, 0, +1, ...
424 for ctl, params in self.Pannings.items():
425 path = params[0]
426 idx = params[1]
427 curr = self.hw.getContignuous(path, idx)
428 state = -(curr / 0x7FFE) * 50 + 50
429 ctl.setValue(state)
430 QObject.connect(ctl, SIGNAL('valueChanged(int)'), self.updatePanning)
432 for ctl, params in self.Volumes.items():
433 path = params[0]
434 idx = params[1]
435 pair = params[2]
436 p_idx = params[3]
437 link = params[4]
439 db = self.hw.getContignuous(path, idx)
440 vol = self.db2vol(db)
441 ctl.setValue(vol)
442 QObject.connect(ctl, SIGNAL('valueChanged(int)'), self.updateVolume)
444 # to activate link button, a pair is checked twice, sign...
445 pair_db = self.hw.getContignuous(path, p_idx)
446 if pair_db== db:
447 link.setChecked(True)
449 for ctl, params in self.Mutes.items():
450 QObject.connect(ctl, SIGNAL('clicked(bool)'), self.updateMute)
452 for ctl, params in self.Mixers.items():
453 path = params[0]
454 in_id = params[1]
455 mix_in_idx = params[2]
456 mix_out_idx = params[3]
457 in_ch_l = self.mixers[self.id][3][mix_in_idx][0]
458 out_ch_l = self.mixers[self.id][1][mix_out_idx][0]
459 # see /libffado/src/bebob/bebob_mixer.cpp
460 mux_id = self.getMultiplexedId(in_id, in_ch_l, out_ch_l)
461 curr = self.hw.getContignuous(path, mux_id);
462 if (curr == 0):
463 state = True
464 else:
465 state = False
466 ctl.setChecked(state)
467 QObject.connect(ctl, SIGNAL('clicked(bool)'), self.updateMixer)
469 if self.id == 3:
470 self.read410HP()
472 # helper functions
473 def vol2db(self, vol):
474 return (log10(vol + 1) - 2) * 16384
476 def db2vol(self, db):
477 return pow(10, db / 16384 + 2) - 1
479 def getMultiplexedId(self, in_id, in_ch_l, out_ch_l):
480 # see /libffado/src/bebob/bebob_mixer.cpp
481 return (in_id << 8) | (in_ch_l << 4) | (out_ch_l << 0)
483 def updateSelector(self, state):
484 sender = self.sender()
485 path = self.Selectors[sender][0]
486 log.debug("set %s to %d" % (path, state))
487 self.hw.setDiscrete(path, state)
489 def updatePanning(self, state):
490 sender = self.sender()
491 path = self.Pannings[sender][0]
492 idx = self.Pannings[sender][1]
493 value = (state - 50) * 0x7FFE / -50
494 log.debug("set %s for %d to %d(%d)" % (path, idx, value, state))
495 self.hw.setContignuous(path, value, idx)
497 def updateVolume(self, vol):
498 sender = self.sender()
499 path = self.Volumes[sender][0]
500 idx = self.Volumes[sender][1]
501 pair = self.Volumes[sender][2]
502 p_idx = self.Volumes[sender][3]
503 link = self.Volumes[sender][4]
505 db = self.vol2db(vol)
506 log.debug("set %s for %d to %d(%d)" % (path, idx, db, vol))
507 self.hw.setContignuous(path, db, idx)
509 if link.isChecked():
510 pair.setValue(vol)
512 # device remeber gain even if muted
513 def updateMute(self, state):
514 sender = self.sender()
515 l_sld = self.Mutes[sender][0]
516 r_sld = self.Mutes[sender][1]
518 if state:
519 db = 0x8000
520 else:
521 db = 0x0000
522 for w in [l_sld, r_sld]:
523 path = self.Volumes[w][0]
524 self.hw.setContignuous(self.Volumes[w][0], db)
525 w.setDisabled(state)
527 def updateMixer(self, checked):
528 if checked:
529 state = 0x0000
530 else:
531 state = 0x8000
533 sender = self.sender()
534 path = self.Mixers[sender][0]
535 in_id = self.Mixers[sender][1]
536 mix_in_idx = self.Mixers[sender][2]
537 mix_out_idx = self.Mixers[sender][3]
538 in_ch_l = self.mixers[self.id][3][mix_in_idx][0]
539 out_ch_l = self.mixers[self.id][1][mix_out_idx][0]
541 mux_id = self.getMultiplexedId(in_id, in_ch_l, out_ch_l)
543 log.debug("set %s for 0x%04X(%d/%d/%d) to %d" % (path, mux_id, in_id, in_ch_l, out_ch_l, state))
544 self.hw.setContignuous(path, state, mux_id)
546 def read410HP(self):
547 path = "/Mixer/Selector_7"
548 sel = self.hw.getDiscrete(path)
549 if sel > 0:
550 enbl = 5
551 else:
552 enbl = -1
554 path = "/Mixer/EnhancedMixer_7"
555 for i in range(5):
556 in_id = 0
557 in_ch_l = i * 2 + 1
558 out_ch_l = 1
559 mux_id = self.getMultiplexedId(in_id, in_ch_l, out_ch_l)
560 state = self.hw.getContignuous(path, mux_id)
561 if enbl < 0 and state == 0:
562 enbl = i
563 else:
564 self.hw.setContignuous(path, 0x8000, mux_id)
565 # if inconsistency between Selector and Mixer, set AUX as default
566 if enbl == -1:
567 self.hw.setDiscrete('/Mixer/Selector_7', 1);
568 enbl = 5
570 self.FW410HP.setCurrentIndex(enbl)
572 def update410HP(self, state):
573 hp_id = self.hp_src[self.id][0][0]
575 # each output from mixer can be multiplexed in headphone
576 # but here they are exclusive because of GUI simpleness, sigh...
577 path = "/Mixer/EnhancedMixer_7"
578 for i in range(5):
579 in_id = 0
580 in_ch_l = i * 2 + 1
581 out_ch_l = 1
582 mux_id = self.getMultiplexedId(in_id, in_ch_l, out_ch_l)
583 if i == state:
584 value = 0x0000
585 else:
586 value = 0x8000
587 self.hw.setContignuous(path, value, mux_id)
589 # Mixer/Aux is selectable exclusively
590 path = "/Mixer/Selector_7"
591 sel = (state == 5)
592 self.hw.setDiscrete(path, sel)