There will be some "packs" available in a few days, amongst others a MIDI Utility pack which contains a script that can do this. It's designed for transposing, but also do rechannelising, and keeps track of the note number and channel of a NoteOn and maps any subsequent NoteOff (and optionally key aftertouch) to the right note and channel.
Take the code below, copy it into an empty script overwriting the existing text and run compile (ctrl-F9].
Code: Select all
(*****************************************************************************************************
Transpose.script
Script for inserting into a MIDI stream to transpose all or just a part of the notes received.
NoteOffs are mapped to the same channel and note number as the previous NoteOns, avoiding
hanging notes even when changing parameters before the note is released.
The input channel/note can be sent together with the transposed notes.
Key/poly aftertouch can optionally also be transposed together with the notes.
*****************************************************************************************************)
CONST BUFFER_SIZE = 128;
// MIDI channel messages
CONST NOTEOFF = Byte(128);
CONST NOTEON = Byte(144);
CONST KA = Byte(160); // Key (poly) Aftertouch
TYPE tMidiArr = ARRAY OF tMidi;
TYPE tNoteOns = RECORD
origChannel : Byte;
origNoteNo : Byte;
outChannel : Byte;
outNoteNo : Byte;
hasKA : Boolean;
END;
////////////////////////////////////////////////////////////////////////////////////////
// In-/out parameters
VAR pMidiIn, pMidiOut1 : tParameter;
VAR pBypass, pChIn, pTrspLoLim, pTrspHiLim, pInclKA : tParameter;
VAR pChOut, pTranspose, pOctFoldBack, pKeep : tParameter;
// Global variables
VAR bypass, inclKA : Boolean;
VAR sentNoteOns : ARRAY OF tNoteOns;
VAR numIn, numSentNoteOns, numOut1 : Integer;
VAR chIn, trspLoLim, trspHiLim, chOut : Byte;
VAR transpose : Integer;
VAR octFoldback, keepOrig : Boolean;
////////////////////////////////////////////////////////////////////////////////////////
PROCEDURE Init;
VAR i : Integer;
BEGIN
trspLoLim := 0;
trspHiLim := 127;
chIn := 0;
chOut := 0;
pMidiIn := CreateParam('midi in', ptMidi); SetIsOutput(pMidiIn, FALSE);
pMidiOut1 := CreateParam('midi out', ptMidi); SetIsInput(pMidiOut1, FALSE);
pBypass := CreateParam('bypass', ptSwitch); SetIsOutput(pBypass, FALSE);
pChIn := CreateParam('channel in', ptListbox); SetIsOutput(pChIn, FALSE);
SetListboxString(pChIn, '"all","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16"');
SetValue(pChIn, Single(chIn));
pTrspLoLim := CreateParam('note lo limit', ptMidiNoteFader); SetIsOutput(pTrspLoLim, FALSE);
SetDefaultValue(pTrspLoLim, trspLoLim); SetValue(pTrspLoLim, trspLoLim);
pTrspHiLim := CreateParam('note hi limit', ptMidiNoteFader); SetIsOutput(pTrspHiLim, FALSE);
SetDefaultValue(pTrspHiLim, trspHiLim); SetValue(pTrspHiLim, trspHiLim);
pInclKA := CreateParam('incl KA', ptSwitch); SetIsOutput(pInclKA, FALSE);
pChOut := CreateParam('channel out', ptListbox); SetIsOutput(pChOut, FALSE);
SetListboxString(pChOut, '"orig","1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16"');
SetValue(pChOut, Single(chOut));
pTranspose := CreateParam('transpose', ptDataFader); SetIsOutput(pTranspose, FALSE);
SetMin(pTranspose, -48); SetMax(pTranspose, 48);
SetFormat(pTranspose, '%.0f'); SetSymbol(pTranspose, 'sm');
pOctFoldback := CreateParam('oct foldback', ptSwitch); SetIsOutput(pOctFoldback, FALSE);
pKeep := CreateParam('keep input', ptSwitch); SetIsOutput(pKeep, FALSE);
SetArrayLength(sentNoteOns, BUFFER_SIZE);
numSentNoteOns := 0;
numOut1 := 0;
END; // Init
///////////////////////////////// Checking channel message types /////////////////////////////////
FUNCTION IsNote (currMsg : tMidi) : Boolean;
BEGIN
IsNote := ((currMsg.msg = NOTEON) OR (currMsg.msg = NOTEOFF));
END;
FUNCTION IsNoteOn (currMsg : tMidi) : Boolean;
BEGIN
IsNoteOn := ((currMsg.msg = NOTEON) AND (currMsg.data2 > 0));
END;
FUNCTION IsNoteOff (currMsg : tMidi) : Boolean;
BEGIN
IsNoteOff := (currMsg.msg = NOTEOFF) OR ((currMsg.msg = NOTEON) AND (currMsg.data2 = 0));
END;
FUNCTION IsKA (currMsg : tMidi) : Boolean;
BEGIN
IsKA := (currMsg.msg = KA);
END;
////////////////////////////////////////////////////////////////////////////////////////////////////
PROCEDURE mTrace(s : String; m : tMidi);
BEGIN
WITH m DO
WriteLn(s + ': msg:' + IntToStr(msg)
+ ' ch:' + IntToStr(channel)
+ ' d1:' + IntToStr(data1)
+ ' d2:' + IntToStr(data2));
END; // mTrace
////////////////////////////////////////////////////////////////////////////////////////////////////
FUNCTION WithinRange(value : Byte) : Boolean;
VAR retVal : Boolean;
BEGIN
retVal := FALSE;
IF ((value >= trspLoLim) AND (value <= trspHiLim)) THEN BEGIN
retVal := TRUE;
END
ELSE IF (value = trspLoLim) OR (value = trspHiLim) THEN BEGIN
retVal := TRUE;
END
ELSE IF (trspLoLim > trspHiLim) THEN BEGIN
retVal := ((value < trspHiLim) OR (value > trspLoLim));
END;
WithinRange := retVal;
END; // WithinRange
////////////////////////////////////////////////////////////////////////////////////////////////////
FUNCTION CalcTranspose(NoteNo : Byte) : Byte;
VAR note : Integer;
BEGIN
note := Integer(NoteNo) + transpose;
IF (note < 0) THEN BEGIN
IF (NOT octFoldback) THEN BEGIN
note := 0;
END
ELSE BEGIN
note := abs(12 + note) MOD 12;
END;
END
ELSE IF (note > 127) THEN BEGIN
IF (NOT octFoldback) THEN BEGIN
note := 127;
END
ELSE BEGIN
note := ((note - 128) MOD 12) + 116;
END;
END;
CalcTranspose := Byte(note);
END; // CalcTranspose
////////////////////////////////////////////////////////////////////////////////////////////////////
PROCEDURE CreateOut(currMsg : tMidi);
BEGIN
SetMidiArrayValue(pMidiOut1, numOut1, currMsg);
numOut1 := numOut1 + 1;
END; // CreateOut
////////////////////////////////////////////////////////////////////////////////////////////////////
PROCEDURE CreateNoteOnOut(origMsg, outMsg : tMidi);
VAR curr : tNoteOns;
BEGIN
WITH curr DO BEGIN
origChannel := origMsg.channel;
origNoteNo := origMsg.data1;
outChannel := outMsg.channel;
outNoteNo := outMsg.data1;
hasKA := FALSE;
END;
sentNoteOns[numSentNoteOns] := curr;
numSentNoteOns := numSentNoteOns + 1;
CreateOut(outMsg);
END; // CreateNoteOnOut
///////////////////////////////////////////////////////////////////////////////////////////////////
PROCEDURE CheckSendNoteOff(currMsg : tMidi);
VAR i, j : Integer;
VAR outMsg : tMidi;
VAR curr : tNoteOns;
VAR found : Boolean;
BEGIN
j := 0;
found := FALSE;
FOR i := 0 TO (numSentNoteOns - 1) DO BEGIN
curr := sentNoteOns[i];
IF ((currMsg.channel = curr.origChannel) AND (currMsg.data1 = curr.origNoteNo)) THEN BEGIN
outMsg.msg := currMsg.msg;
outMsg.channel := curr.outChannel;
outMsg.data1 := curr.outNoteNo;
outMsg.data2 := currMsg.data2;
CreateOut(outMsg);
IF (curr.hasKA) THEN BEGIN
outMsg.msg := KA;
outMsg.data2 := 0;
CreateOut(outMsg); // Resets any key aftertouch <> 0
END;
j := j + 1;
found := TRUE;
END
ELSE BEGIN
sentNoteOns[i - j] := curr;
END;
END;
numSentNoteOns := numSentNoteOns - j;
IF (NOT found) THEN CreateOut(currMsg);
END; // CheckSendNoteOff
////////////////////////////////////////////////////////////////////////////////////////////////////
PROCEDURE CheckSendKA(currMsg : tMidi);
VAR i : Integer;
VAR outMsg : tMidi;
VAR curr : tNoteOns;
BEGIN
FOR i := 0 TO (numSentNoteOns - 1) DO BEGIN
curr := sentNoteOns[i];
IF ((currMsg.channel = curr.origChannel) AND (currMsg.data1 = curr.origNoteNo)) THEN BEGIN
outMsg.msg := currMsg.msg;
outMsg.channel := curr.outChannel;
outMsg.data1 := curr.outNoteNo;
outMsg.data2 := currMsg.data2;
sentNoteOns[i].hasKA := TRUE;
CreateOut(outMsg);
END;
END;
END; // CheckSendKA
////////////////////////////////////////////////////////////////////////////////////////////////////
PROCEDURE CheckInput(currMsg : tMidi);
VAR newMsg : tMidi;
BEGIN
IF (bypass AND IsNoteOn(currMsg)) THEN BEGIN
CreateNoteOnOut(currMsg, currMsg);
END
ELSE IF ( (NOT bypass)
AND IsNoteOn(currMsg)
AND ((chIn = 0) OR (chIn = currMsg.channel))
AND WithinRange(currMsg.data1)) THEN BEGIN
newMsg := currMsg;
IF (transpose <> 0) THEN
newMsg.data1 := CalcTranspose(newMsg.data1);
IF (chOut <> 0) THEN
newMsg.channel := chOut;
IF (keepOrig AND ((currMsg.channel <> newMsg.channel) OR (currMsg.data1 <> newMsg.data1))) THEN
CreateNoteOnOut(currMsg, currMsg);
CreateNoteOnOut(currMsg, newMsg);
END
ELSE IF (IsNoteOff(currMsg)) THEN BEGIN
CheckSendNoteOff(currMsg);
END
ELSE IF (inclKA AND IsKA(currMsg)) THEN BEGIN
CheckSendKA(currMsg);
END
ELSE BEGIN
CreateOut(currMsg);
END;
END; // CheckInput
////////////////////////////////////////////////////////////////////////////////////////////////////
PROCEDURE Callback(n : Integer);
BEGIN
//strace('callback: ' + IntToStr(n));
CASE n OF
pMidiIn : numIn := GetLength(n);
pBypass : bypass := (GetValue(n) > 0);
pChIn : chIn := Byte(trunc(GetValue(n)));
pTrspLoLim : trspLoLim := Byte(trunc(GetValue(n)));
pTrspHiLim : trspHiLim := Byte(trunc(GetValue(n)));
pInclKA : inclKA := (GetValue(n) > 0);
pChOut : chOut := Byte(trunc(GetValue(n)));
pTranspose : transpose := round(GetValue(n));
pOctFoldback : octFoldback := (GetValue(n) > 0);
pKeep : keepOrig := (GetValue(n) > 0);
END;
END; // Callback
////////////////////////////////////////////////////////////////////////////////////////////////////
PROCEDURE Process;
VAR i : Integer;
VAR currMsg : tMidi;
BEGIN
FOR i := 0 TO (numIn - 1) DO BEGIN
GetMidiArrayValue(pMidiIn, i, currMsg);
CheckInput(currMsg);
END;
SetLength(pMidiOut1, numOut1);
numOut1 := 0;
END; // Process
(Hope it's the right version - I'm away from home and haven't got time to check)