From b1a04d7f76bcc214deba3dc4105cc269fdc959af Mon Sep 17 00:00:00 2001 From: falkTX Date: Sun, 10 Nov 2019 17:23:16 +0100 Subject: [PATCH] Start fixing up midi pattern plugin, WIP --- source/frontend/widgets/pianoroll.py | 86 ++++++--------- source/native-plugins/midi-base.hpp | 2 +- source/native-plugins/midi-pattern.cpp | 62 ++++++----- .../native-plugins/resources/midipattern-ui | 102 +++++++++++------- 4 files changed, 134 insertions(+), 118 deletions(-) diff --git a/source/frontend/widgets/pianoroll.py b/source/frontend/widgets/pianoroll.py index 3e19d724b..2ff1b7334 100755 --- a/source/frontend/widgets/pianoroll.py +++ b/source/frontend/widgets/pianoroll.py @@ -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() diff --git a/source/native-plugins/midi-base.hpp b/source/native-plugins/midi-base.hpp index afc95156b..165601252 100644 --- a/source/native-plugins/midi-base.hpp +++ b/source/native-plugins/midi-base.hpp @@ -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; diff --git a/source/native-plugins/midi-pattern.cpp b/source/native-plugins/midi-pattern.cpp index 82aa0683c..2c25e6abc 100644 --- a/source/native-plugins/midi-pattern.cpp +++ b/source/native-plugins/midi-pattern.cpp @@ -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(fParameters[kParameterMeasures]) /2; // FIXME: why /2 ? + fMaxTicks = TICKS_PER_BEAT * fTimeSigNum * static_cast(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(fTimeInfo.frame); const double endPos = playPos + fTicksPerFrame*static_cast(frames); - const double loopedEndPos = std::fmod(endPos, fMaxTicks); + const double loopedEndPos = std::fmod(endPos, fMaxTicks); + + /* + for (uint32_t i=0; i(std::fmod(fTicksPerFrame * static_cast(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(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(ticksPerFrame * static_cast(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(beatsPerMinute), - static_cast(beatsPerBar), - static_cast(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(fParameters[kParameterTimeSig]), + static_cast(fParameters[kParameterMeasures]), + static_cast(fParameters[kParameterDefLength]), + static_cast(fParameters[kParameterQuantize])); + writeMessage(strBuf); + for (LinkedList::Itenerator it = fMidiOut.iteneratorBegin(); it.valid(); it.next()) { const RawMidiEvent* const rawMidiEvent(it.getValue(nullptr)); diff --git a/source/native-plugins/resources/midipattern-ui b/source/native-plugins/resources/midipattern-ui index 64fbd2313..a3b1ed40b 100755 --- a/source/native-plugins/resources/midipattern-ui +++ b/source/native-plugins/resources/midipattern-ui @@ -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