| @@ -332,7 +332,7 @@ class PianoRoll(QGraphicsScene): | |||
| self.quantize_val = quantize_val | |||
| ### dummy vars that will be changed | |||
| self.time_sig = 0 | |||
| self.time_sig = (0,0) | |||
| self.measure_width = 0 | |||
| self.num_measures = 0 | |||
| self.max_note_length = 0 | |||
| @@ -343,8 +343,6 @@ class PianoRoll(QGraphicsScene): | |||
| self.header = None | |||
| self.play_head = None | |||
| self.setTimeSig(time_sig) | |||
| self.setMeasures(num_measures) | |||
| self.setGridDiv() | |||
| self.default_length = 1. / self.grid_div | |||
| @@ -352,47 +350,38 @@ class PianoRoll(QGraphicsScene): | |||
| # ------------------------------------------------------------------------- | |||
| # Callbacks | |||
| def movePlayHead(self, transport_info): | |||
| # TODO: need conversion between frames and PPQ | |||
| x = 105. # works for 120bpm | |||
| total_duration = self.time_sig[0] * self.num_measures * x | |||
| pos = transport_info['frame'] / x | |||
| frac = (pos % total_duration) / total_duration | |||
| def movePlayHead(self, transportInfo): | |||
| ticksPerBeat = transportInfo['ticksPerBeat'] | |||
| max_ticks = ticksPerBeat * self.time_sig[0] * self.num_measures | |||
| cur_tick = ticksPerBeat * self.time_sig[0] * transportInfo['bar'] + ticksPerBeat * transportInfo['beat'] + transportInfo['tick'] | |||
| frac = (cur_tick % max_ticks) / max_ticks | |||
| self.play_head.setPos(QPointF(frac * self.grid_width, 0)) | |||
| def setTimeSig(self, time_sig): | |||
| try: | |||
| new_time_sig = list(map(float, time_sig.split('/'))) | |||
| if len(new_time_sig)==2: | |||
| self.time_sig = new_time_sig | |||
| self.measure_width = self.full_note_width * self.time_sig[0]/self.time_sig[1] | |||
| self.max_note_length = self.num_measures * self.time_sig[0]/self.time_sig[1] | |||
| self.grid_width = self.measure_width * self.num_measures | |||
| self.setGridDiv() | |||
| except ValueError: | |||
| pass | |||
| self.time_sig = time_sig | |||
| self.measure_width = self.full_note_width * self.time_sig[0]/self.time_sig[1] | |||
| self.max_note_length = self.num_measures * self.time_sig[0]/self.time_sig[1] | |||
| self.grid_width = self.measure_width * self.num_measures | |||
| self.setGridDiv() | |||
| def setMeasures(self, measures): | |||
| try: | |||
| self.num_measures = float(measures) | |||
| self.max_note_length = self.num_measures * self.time_sig[0]/self.time_sig[1] | |||
| self.grid_width = self.measure_width * self.num_measures | |||
| self.refreshScene() | |||
| except: | |||
| pass | |||
| #try: | |||
| self.num_measures = float(measures) | |||
| self.max_note_length = self.num_measures * self.time_sig[0]/self.time_sig[1] | |||
| self.grid_width = self.measure_width * self.num_measures | |||
| self.refreshScene() | |||
| #except: | |||
| #pass | |||
| def setDefaultLength(self, length): | |||
| try: | |||
| v = list(map(float, length.split('/'))) | |||
| if len(v) < 3: | |||
| self.default_length = \ | |||
| v[0] if len(v)==1 else \ | |||
| v[0] / v[1] | |||
| pos = self.enforce_bounds(self.mousePos) | |||
| if self.insert_mode: self.makeGhostNote(pos.x(), pos.y()) | |||
| except ValueError: | |||
| pass | |||
| v = list(map(float, length.split('/'))) | |||
| if len(v) < 3: | |||
| self.default_length = \ | |||
| v[0] if len(v)==1 else \ | |||
| v[0] / v[1] | |||
| pos = self.enforce_bounds(self.mousePos) | |||
| if self.insert_mode: | |||
| self.makeGhostNote(pos.x(), pos.y()) | |||
| def setGridDiv(self, div=None): | |||
| if not div: div = self.quantize_val | |||
| @@ -409,16 +398,13 @@ class PianoRoll(QGraphicsScene): | |||
| pass | |||
| def setQuantize(self, value): | |||
| try: | |||
| val = list(map(float, value.split('/'))) | |||
| if len(val) == 1: | |||
| self.quantize(val[0]) | |||
| self.quantize_val = value | |||
| elif len(val) == 2: | |||
| self.quantize(val[0] / val[1]) | |||
| self.quantize_val = value | |||
| except ValueError: | |||
| pass | |||
| val = list(map(float, value.split('/'))) | |||
| if len(val) == 1: | |||
| self.quantize(val[0]) | |||
| self.quantize_val = value | |||
| elif len(val) == 2: | |||
| self.quantize(val[0] / val[1]) | |||
| self.quantize_val = value | |||
| # ------------------------------------------------------------------------- | |||
| # Event Callbacks | |||
| @@ -851,10 +837,9 @@ class ModeIndicator(QWidget): | |||
| self.mode = None | |||
| def paintEvent(self, event): | |||
| painter = QPainter(self) | |||
| event.accept() | |||
| painter.begin(self) | |||
| painter = QPainter(self) | |||
| painter.setPen(QPen(QColor(0, 0, 0, 0))) | |||
| if self.mode == 'velocity_mode': | |||
| @@ -865,9 +850,6 @@ class ModeIndicator(QWidget): | |||
| painter.setBrush(QColor(0, 0, 0, 0)) | |||
| painter.drawRect(0, 0, 30, 20) | |||
| # FIXME | |||
| painter.end() | |||
| def changeMode(self, new_mode): | |||
| self.mode = new_mode | |||
| self.update() | |||
| @@ -291,7 +291,7 @@ public: | |||
| if (fData.count() == 0) | |||
| return nullptr; | |||
| char* const data((char*)std::calloc(1, fData.count()*maxMsgSize)); | |||
| char* const data((char*)std::calloc(1, fData.count()*maxMsgSize+1)); | |||
| CARLA_SAFE_ASSERT_RETURN(data != nullptr, nullptr); | |||
| char* dataWrtn = data; | |||
| @@ -20,6 +20,9 @@ | |||
| #include "midi-base.hpp" | |||
| // matches UI side | |||
| #define TICKS_PER_BEAT 48 | |||
| // ----------------------------------------------------------------------- | |||
| class MidiPatternPlugin : public NativePluginAndUiClass, | |||
| @@ -52,7 +55,7 @@ public: | |||
| fParameters[kParameterDefLength] = 4.0f; | |||
| fParameters[kParameterQuantize] = 4.0f; | |||
| fMaxTicks = 48.0 * fTimeSigNum * 4 /* kParameterMeasures */ / 2; // FIXME: why / 2 ? | |||
| fMaxTicks = TICKS_PER_BEAT * fTimeSigNum * 4 /* kParameterMeasures */; | |||
| } | |||
| protected: | |||
| @@ -202,7 +205,7 @@ protected: | |||
| fTimeSigNum = 1; | |||
| // fall through | |||
| case kParameterMeasures: | |||
| fMaxTicks = 48.0 * fTimeSigNum * static_cast<double>(fParameters[kParameterMeasures]) /2; // FIXME: why /2 ? | |||
| fMaxTicks = TICKS_PER_BEAT * fTimeSigNum * static_cast<double>(fParameters[kParameterMeasures]); | |||
| break; | |||
| } | |||
| } | |||
| @@ -210,7 +213,8 @@ protected: | |||
| // ------------------------------------------------------------------- | |||
| // Plugin process calls | |||
| void process(const float**, float**, const uint32_t frames, const NativeMidiEvent*, uint32_t) override | |||
| void process(const float**, float**, const uint32_t frames, | |||
| const NativeMidiEvent* /*midiEvents*/, uint32_t /*midiEventCount*/) override | |||
| { | |||
| if (const NativeTimeInfo* const timeInfo = getTimeInfo()) | |||
| fTimeInfo = *timeInfo; | |||
| @@ -247,12 +251,20 @@ protected: | |||
| if (! fTimeInfo.bbt.valid) | |||
| fTimeInfo.bbt.beatsPerMinute = 120.0; | |||
| fTicksPerFrame = 48.0 / (60.0 / fTimeInfo.bbt.beatsPerMinute * getSampleRate()); | |||
| fTicksPerFrame = TICKS_PER_BEAT / (60.0 / fTimeInfo.bbt.beatsPerMinute * getSampleRate()); | |||
| /* */ double playPos = fTicksPerFrame*static_cast<double>(fTimeInfo.frame); | |||
| const double endPos = playPos + fTicksPerFrame*static_cast<double>(frames); | |||
| const double loopedEndPos = std::fmod(endPos, fMaxTicks); | |||
| const double loopedEndPos = std::fmod(endPos, fMaxTicks); | |||
| /* | |||
| for (uint32_t i=0; i<midiEventCount; ++i) | |||
| { | |||
| uint32_t pos = static_cast<uint32_t>(std::fmod(fTicksPerFrame * static_cast<double>(fTimeInfo.frame + midiEvents[i].time), fMaxTicks)); | |||
| fMidiOut.addRaw(pos, midiEvents[i].data, midiEvents[i].size); | |||
| } | |||
| */ | |||
| for (; playPos < endPos; playPos += fMaxTicks) | |||
| { | |||
| @@ -290,13 +302,12 @@ protected: | |||
| if (isPipeRunning()) | |||
| { | |||
| char strBuf[0xff+1]; | |||
| strBuf[0xff] = '\0'; | |||
| carla_zeroChars(strBuf, 0xff+1); | |||
| const double beatsPerBar = fTimeInfo.bbt.valid ? static_cast<double>(fTimeInfo.bbt.beatsPerBar) : 4.0; | |||
| const double beatsPerBar = fTimeSigNum; | |||
| const double beatsPerMinute = fTimeInfo.bbt.valid ? fTimeInfo.bbt.beatsPerMinute : 120.0; | |||
| const float beatType = fTimeInfo.bbt.valid ? fTimeInfo.bbt.beatType : 4.0f; | |||
| const double ticksPerBeat = 48.0; | |||
| const double ticksPerBeat = TICKS_PER_BEAT; | |||
| const double ticksPerFrame = ticksPerBeat / (60.0 / beatsPerMinute * getSampleRate()); | |||
| const double fullTicks = static_cast<double>(ticksPerFrame * static_cast<long double>(fTimeInfo.frame)); | |||
| const double fullBeats = fullTicks / ticksPerBeat; | |||
| @@ -307,24 +318,17 @@ protected: | |||
| const CarlaMutexLocker cml(getPipeLock()); | |||
| if (! writeAndFixMessage("transport")) | |||
| return; | |||
| if (! writeMessage(fTimeInfo.playing ? "true\n" : "false\n")) | |||
| return; | |||
| CARLA_SAFE_ASSERT_RETURN(writeMessage("transport\n"),); | |||
| std::sprintf(strBuf, P_UINT64 ":%i:%i:%i\n", fTimeInfo.frame, bar, beat, tick); | |||
| if (! writeMessage(strBuf)) | |||
| return; | |||
| std::snprintf(strBuf, 0xff, "%i:" P_UINT64 ":%i:%i:%i\n", int(fTimeInfo.playing), fTimeInfo.frame, bar, beat, tick); | |||
| CARLA_SAFE_ASSERT_RETURN(writeMessage(strBuf),); | |||
| { | |||
| const CarlaScopedLocale csl; | |||
| std::sprintf(strBuf, "%f:%f:%f\n", | |||
| static_cast<double>(beatsPerMinute), | |||
| static_cast<double>(beatsPerBar), | |||
| static_cast<double>(beatType)); | |||
| std::snprintf(strBuf, 0xff, "%f\n", beatsPerMinute); | |||
| } | |||
| if (! writeMessage(strBuf)) | |||
| return; | |||
| CARLA_SAFE_ASSERT_RETURN(writeMessage(strBuf),); | |||
| flushMessages(); | |||
| } | |||
| @@ -402,7 +406,7 @@ protected: | |||
| data[i] = dvalue; | |||
| } | |||
| fMidiOut.addRaw(time, data, size); | |||
| fMidiOut.addRaw(time /* * TICKS_PER_BEAT */, data, size); | |||
| return true; | |||
| } | |||
| @@ -424,7 +428,7 @@ protected: | |||
| data[i] = dvalue; | |||
| } | |||
| fMidiOut.removeRaw(time, data, size); | |||
| fMidiOut.removeRaw(time /* * TICKS_PER_BEAT */, data, size); | |||
| return true; | |||
| } | |||
| @@ -450,13 +454,21 @@ private: | |||
| void _sendEventsToUI() const noexcept | |||
| { | |||
| char strBuf[0xff+1]; | |||
| strBuf[0xff] = '\0'; | |||
| carla_zeroChars(strBuf, 0xff); | |||
| const CarlaMutexLocker cml1(getPipeLock()); | |||
| const CarlaMutexLocker cml2(fMidiOut.getLock()); | |||
| writeMessage("midi-clear-all\n", 15); | |||
| writeMessage("parameters\n", 11); | |||
| std::snprintf(strBuf, 0xff, "%i:%i:%i:%i\n", | |||
| static_cast<int>(fParameters[kParameterTimeSig]), | |||
| static_cast<int>(fParameters[kParameterMeasures]), | |||
| static_cast<int>(fParameters[kParameterDefLength]), | |||
| static_cast<int>(fParameters[kParameterQuantize])); | |||
| writeMessage(strBuf); | |||
| for (LinkedList<const RawMidiEvent*>::Itenerator it = fMidiOut.iteneratorBegin(); it.valid(); it.next()) | |||
| { | |||
| const RawMidiEvent* const rawMidiEvent(it.getValue(nullptr)); | |||
| @@ -42,7 +42,7 @@ from externalui import ExternalUI | |||
| # ------------------------------------------------------------------------------------------------------------ | |||
| class MidiPatternW(ExternalUI, QMainWindow): | |||
| PPQ = 48.0 | |||
| TICKS_PER_BEAT = 48 | |||
| def __init__(self): | |||
| ExternalUI.__init__(self) | |||
| @@ -54,6 +54,7 @@ class MidiPatternW(ExternalUI, QMainWindow): | |||
| # to be filled with note-on events, while waiting for their matching note-off | |||
| self.fPendingNoteOns = [] # (channel, note, velocity, time) | |||
| self.fTimeSignature = (4,4) | |||
| self.fTransportInfo = { | |||
| "playing": False, | |||
| "frame": 0, | |||
| @@ -61,8 +62,6 @@ class MidiPatternW(ExternalUI, QMainWindow): | |||
| "beat": 0, | |||
| "tick": 0, | |||
| "bpm": 120.0, | |||
| "sigNum": 4.0, | |||
| "sigDenom": 4.0 | |||
| } | |||
| self.ui.act_edit_insert.triggered.connect(self.slot_editInsertMode) | |||
| @@ -79,7 +78,7 @@ class MidiPatternW(ExternalUI, QMainWindow): | |||
| self.ui.defaultLengthBox.currentIndexChanged[int].connect(self.slot_paramChanged) | |||
| self.ui.quantizeBox.currentIndexChanged[int].connect(self.slot_paramChanged) | |||
| self.ui.timeSigBox.currentIndexChanged[str].connect(self.ui.piano.setTimeSig) | |||
| self.ui.timeSigBox.currentIndexChanged[str].connect(self.slot_setTimeSignature) | |||
| self.ui.measureBox.currentIndexChanged[str].connect(self.ui.piano.setMeasures) | |||
| self.ui.defaultLengthBox.currentIndexChanged[str].connect(self.ui.piano.setDefaultLength) | |||
| self.ui.quantizeBox.currentIndexChanged[str].connect(self.ui.piano.setGridDiv) | |||
| @@ -132,6 +131,18 @@ class MidiPatternW(ExternalUI, QMainWindow): | |||
| self.sendControl(param, index) | |||
| def slot_setTimeSignature(self, sigtext): | |||
| try: | |||
| timesig = tuple(map(float, sigtext.split('/'))) | |||
| except ValueError: | |||
| pass | |||
| if len(timesig) != 2: | |||
| return | |||
| self.fTimeSignature = timesig | |||
| self.ui.piano.setTimeSig(timesig) | |||
| # ------------------------------------------------------------------- | |||
| # DSP Callbacks | |||
| @@ -139,13 +150,28 @@ class MidiPatternW(ExternalUI, QMainWindow): | |||
| value = int(value) | |||
| if index == 0: # TimeSig | |||
| self.ui.timeSigBox.blockSignals(True) | |||
| self.ui.timeSigBox.setCurrentIndex(value) | |||
| self.slot_setTimeSignature(self.ui.timeSigBox.currentText()) | |||
| self.ui.timeSigBox.blockSignals(False) | |||
| elif index == 1: # Measures | |||
| self.ui.measureBox.blockSignals(True) | |||
| self.ui.measureBox.setCurrentIndex(value-1) | |||
| self.ui.piano.setMeasures(self.ui.measureBox.currentText()) | |||
| self.ui.measureBox.blockSignals(False) | |||
| elif index == 2: # DefLength | |||
| self.ui.defaultLengthBox.blockSignals(True) | |||
| self.ui.defaultLengthBox.setCurrentIndex(value) | |||
| self.ui.piano.setDefaultLength(self.ui.defaultLengthBox.currentText()) | |||
| self.ui.defaultLengthBox.blockSignals(False) | |||
| elif index == 3: # Quantize | |||
| self.ui.quantizeBox.blockSignals(True) | |||
| self.ui.quantizeBox.setCurrentIndex(value) | |||
| self.ui.piano.setQuantize(self.ui.quantizeBox.currentText()) | |||
| self.ui.quantizeBox.blockSignals(False) | |||
| def dspStateChanged(self, key, value): | |||
| pass | |||
| @@ -193,21 +219,21 @@ class MidiPatternW(ExternalUI, QMainWindow): | |||
| # Custom callback | |||
| def updateMeasureBox(self, index): | |||
| self.measureBox.setCurrentIndex(index-1) | |||
| self.ui.measureBox.setCurrentIndex(index-1) | |||
| def sendMsg(self, data): | |||
| msg = data[0] | |||
| if msg == "midievent-remove": | |||
| if msg == "midievent-add": | |||
| note, start, length, vel = data[1:5] | |||
| note_start = start * 60. / self.fTransportInfo["bpm"] * 4. / self.fTransportInfo["sigDenom"] * self.PPQ | |||
| note_stop = note_start + length * 60. / self.fTransportInfo["bpm"] * 4. * self.fTransportInfo["sigNum"] / self.fTransportInfo["sigDenom"] * self.PPQ | |||
| note_start = start * 60. / self.fTransportInfo["bpm"] * self.TICKS_PER_BEAT | |||
| note_stop = note_start + length * 60. / self.fTransportInfo["bpm"] * 4. * self.fTimeSignature[0] / self.fTimeSignature[1] * self.TICKS_PER_BEAT | |||
| self.send([msg, note_start, 3, MIDI_STATUS_NOTE_ON, note, vel]) | |||
| self.send([msg, note_stop, 3, MIDI_STATUS_NOTE_OFF, note, vel]) | |||
| elif msg == "midievent-add": | |||
| elif msg == "midievent-remove": | |||
| note, start, length, vel = data[1:5] | |||
| note_start = start * 60. / self.fTransportInfo["bpm"] * self.PPQ | |||
| note_stop = note_start + length * 60. / self.fTransportInfo["bpm"] * 4. * self.fTransportInfo["sigNum"] / self.fTransportInfo["sigDenom"] * self.PPQ | |||
| note_start = start * 60. / self.fTransportInfo["bpm"] * self.TICKS_PER_BEAT # 4. / self.fTransportInfo["sigDenom"] * self.TICKS_PER_BEAT | |||
| note_stop = note_start + length * 60. / self.fTransportInfo["bpm"] * 4. * self.fTimeSignature[0] / self.fTimeSignature[1] * self.TICKS_PER_BEAT | |||
| self.send([msg, note_start, 3, MIDI_STATUS_NOTE_ON, note, vel]) | |||
| self.send([msg, note_stop, 3, MIDI_STATUS_NOTE_OFF, note, vel]) | |||
| @@ -222,20 +248,14 @@ class MidiPatternW(ExternalUI, QMainWindow): | |||
| # adds single midi event | |||
| time = int(self.readlineblock()) | |||
| size = int(self.readlineblock()) | |||
| data = [] | |||
| for x in range(size): | |||
| data.append(int(self.readlineblock())) | |||
| data = tuple(int(self.readlineblock()) for x in range(size)) | |||
| self.handleMidiEvent(time, size, data) | |||
| elif msg == "transport": | |||
| playing = bool(self.readlineblock() == "true") | |||
| frame, bar, beat, tick = [int(i) for i in self.readlineblock().split(":")] | |||
| bpm, sigNum, sigDenom = [float(i) for i in self.readlineblock().split(":")] | |||
| if beat != self.fTransportInfo["beat"]: | |||
| print(beat) | |||
| playing, frame, bar, beat, tick = tuple(int(i) for i in self.readlineblock().split(":")) | |||
| bpm = float(self.readlineblock()) | |||
| playing = bool(int(playing)) | |||
| old_frame = self.fTransportInfo['frame'] | |||
| @@ -246,13 +266,19 @@ class MidiPatternW(ExternalUI, QMainWindow): | |||
| "beat": beat, | |||
| "tick": tick, | |||
| "bpm": bpm, | |||
| "sigNum": sigNum, | |||
| "sigDenom": sigDenom | |||
| "ticksPerBeat": self.TICKS_PER_BEAT, | |||
| } | |||
| if old_frame != frame: | |||
| self.ui.piano.movePlayHead(self.fTransportInfo) | |||
| elif msg == "parameters": | |||
| timesig, measures, deflength, quantize = tuple(int(i) for i in self.readlineblock().split(":")) | |||
| self.dspParameterChanged(0, timesig) | |||
| self.dspParameterChanged(1, measures) | |||
| self.dspParameterChanged(2, deflength) | |||
| self.dspParameterChanged(3, quantize) | |||
| else: | |||
| ExternalUI.msgCallback(self, msg) | |||
| @@ -260,16 +286,11 @@ class MidiPatternW(ExternalUI, QMainWindow): | |||
| # Internal stuff | |||
| def handleMidiEvent(self, time, size, data): | |||
| #print("Got MIDI Event on UI", time, size, data) | |||
| # NOTE: for now time comes in frames, which might not be desirable | |||
| # we'll convert it to a smaller value for now (seconds) | |||
| # later on we can have time as PPQ or similar | |||
| time /= self.PPQ | |||
| #print("handleMidiEvent", time, size, data) | |||
| status = MIDI_GET_STATUS_FROM_DATA(data) | |||
| channel = MIDI_GET_CHANNEL_FROM_DATA(data) | |||
| if status == MIDI_STATUS_NOTE_ON: | |||
| note = data[1] | |||
| velo = data[2] | |||
| @@ -279,27 +300,28 @@ class MidiPatternW(ExternalUI, QMainWindow): | |||
| elif status == MIDI_STATUS_NOTE_OFF: | |||
| note = data[1] | |||
| velo = data[2] | |||
| # find previous note-on that matches this note and channel | |||
| for noteOnMsg in self.fPendingNoteOns: | |||
| channel_, note_, velo_, time_ = noteOnMsg | |||
| on_channel, on_note, on_velo, on_time = noteOnMsg | |||
| if channel_ != channel: | |||
| if on_channel != channel: | |||
| continue | |||
| if note_ != note: | |||
| if on_note != note: | |||
| continue | |||
| # found it | |||
| #print("{} {} {} {}\n".format(note, time_, time-time_, velo_)) | |||
| start = time_ / 60. * self.fTransportInfo["bpm"] / 4. * self.fTransportInfo["sigDenom"] | |||
| length = (time - time_) / 60. * self.fTransportInfo["bpm"] / 4. / self.fTransportInfo["sigNum"] * self.fTransportInfo["sigDenom"] | |||
| self.ui.piano.drawNote(note, start, length, velo_) | |||
| # remove from list | |||
| self.fPendingNoteOns.remove(noteOnMsg) | |||
| break | |||
| else: | |||
| return | |||
| self.ui.piano.drawNote(note, | |||
| on_time/self.TICKS_PER_BEAT, | |||
| (time-on_time)/self.TICKS_PER_BEAT/self.fTimeSignature[0], on_velo) | |||
| #--------------- main ------------------ | |||
| if __name__ == '__main__': | |||
| import resources_rc | |||