colinfwren / midi-to-lsdj

Convert a MIDI file into LSDJ project
GNU Affero General Public License v3.0
5 stars 0 forks source link

Add means to define pitch-bend/sweep for pitch-bent notes #17

Closed colinfwren closed 1 year ago

colinfwren commented 1 year ago

Add support for pitch-bending (pitchBend in midi-file) and map that to a either a P or S command (allow user to define this?)

LSDJ Manual for P

4.13 P: Pitch Bend

4.13.1 For Pulse, Wave and Kit Instruments:
Does a pitch change with the given speed. The behavior depends on the instru-
ment’s PITCH setting:
DRUM Logarithmic pitch bend that updates at 360 Hz.
FAST Linear pitch bend that updates at 360 Hz.
TICK Pitch bend that updates every tick.
STEP Immediate pitch change without bend.

Example:
P02 Pitch change up with speed 2.
PFE Pitch change down with speed 2. (FE=-2)

LSDJ manual for S

4.15 S: Sweep/Shape
This command has different effects for different instrument types.

4.15.1 Pulse Instruments
Frequency sweep, useful for bass drums and percussion. The first digit sets
time, the second sets pitch increase/decrease. Only works on the first pulse
channel.

4.15.2 Kit Instruments
S changes the loop points. The first digit modulates the offset value; the second
digit modulates the loop length. (1-7=increase, 9-F=decrease.) Used creatively,
this command can be very useful for creating a wide range of percussive and
timbral effects.

4.15.3 Noise Instruments
Alters noise shape (see section 2.6.5). The command is relative, meaning that
the digits are independently added to the active noise shape.
colinfwren commented 1 year ago

In tonejs/midi pitch bend events are a separate array in the track, the ticks of these won't align with a specific note tick so will need to calculate if a pitch bend falls between the notes duration and then if does add the command.

Looks like the pitchbend value is between -2 and 2 based on the logic in @tonejs/midi's addPitchBend function:

this.addPitchBend({
    ticks: event.absoluteTime,
    // Scale the value between -2^13 to 2^13 to -2 to 2.
    value: event.value / Math.pow(2, 13),
});

The event.value comes from midi-file which maps the pitch bend 7-bit value to a number between 0 and 16383 based on the MIDI spec, which uses the middle value 8192 as 0 with values below it being a negative pitch bend and those above it being a postive pitch bend.

To map this to pulse/sweep there will need to be a calculation of note duration and pitch to find the appropriate speed

Sweep seems to be the easiest option as can create a maps of bend durations to first digit and bend pitch to second digit.

Implementation logic for pitch:

const pitchVals = [0.3, 0.6, 0.9, 1.2, 1.5, 1.8, 2]
const fullPitchVals = [0, 0.3, 0.6, 0.9, 1.2, 1.5, 1.8, 2, 0, -2, -1.8, -1.5, -1.2, -0.9, -0.6, -0.3]
const pitchBendVal = 0.015625
const mappedPitchVal = pitchVals.reduce((prev, curr) => Math.abs(curr - pitchBendVal) < Math.abs(prev - pitchBendVal) ? curr : prev)
const signedMappedPitchVal = pitchBendVal < 0 ? mappedPitchVal * -1 : mappedPitchVal
const mappedPitchValIndex = fullPitchVals.indexOf(signedMappedPitchVal)
const pitchAsHex = convertToHex(mappedPitchValIndex)

implementation for speed:

const ppq = data.header.ppq // normally 480
const noteTickLengths = [0, 4, 2, 1, 0.5, 0.25, 0.125, 0.0625].map((duration) => ppq * duration)
const mappedNoteDuration = noteTickLengths.reduce((prev, curr) => Math.abs(curr - note.durationTicks) < Math.abs(prev - note.durationTicks) ? curr : prev)
const noteTickIndex = noteTickLengths.indexOf(mappedNoteDuration)
const speedAsHex = convertToHex((noteTickIndex  + 1)* 2)