@@ -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 | |||