Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added ADSR envelope with velocity sensitivity #1832

Merged
merged 3 commits into from Mar 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
- Added built-in attributes for Mesh object.
- Added Vertex Group as attribute for Mesh object.
- Added *Scene Changed* auto execution option.
- Added full ADSR (Attack, Delay, Sustain, Release) envelope to Evaluate Midi Track Node
- Added velocity sensitivity to Evaluate Midi Track Node

### Fixed

Expand Down
40 changes: 28 additions & 12 deletions animation_nodes/data_structures/midi/midi_note.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
from dataclasses import dataclass

def evaluateEnvelope(time, timeOn, timeOff, attackTime, attackInterpolation, decayTime, decayInterpolation, sustainLevel):
# find either point in time for envelope or where in envelope the timeOff happened
relativeTime = min(time, timeOff) - timeOn

if relativeTime <= 0.0:
return 0.0

if relativeTime < attackTime:
return attackInterpolation(relativeTime / attackTime)

relativeTime = relativeTime - attackTime

if relativeTime < decayTime:
decayNormalized = decayInterpolation(1 - relativeTime/ decayTime)
return decayNormalized * (1 - sustainLevel) + sustainLevel

return sustainLevel

@dataclass
class MIDINote:
channel: int = 0
Expand All @@ -8,18 +26,16 @@ class MIDINote:
timeOff: float = 0
velocity: float = 0

def evaluate(self, time, attackTime, attackInterpolation, releaseTime, releaseInterpolation):
peakTime = self.timeOn + attackTime
endTime = self.timeOff + releaseTime
if self.timeOff >= time >= peakTime:
return 1
elif peakTime > time >= self.timeOn:
if attackTime == 0: return 1
return attackInterpolation((time - self.timeOn) / attackTime)
elif endTime >= time > self.timeOff:
if releaseTime == 0: return 1
return releaseInterpolation(1 - ((time - self.timeOff) / releaseTime))
return 0.0
def evaluate(self, time, attackTime, attackInterpolation, decayTime, decayInterpolation, sustainLevel,
releaseTime, releaseInterpolation, velocitySensitivity):

value = evaluateEnvelope(time, self.timeOn, self.timeOff, attackTime, attackInterpolation, decayTime, decayInterpolation, sustainLevel)

if time > self.timeOff:
OmarEmaraDev marked this conversation as resolved.
Show resolved Hide resolved
value = value * releaseInterpolation(1 - ((time - self.timeOff) / releaseTime))

# if velocity sensitivity is 25%, then take 75% of envelope and 25% of envelope with velocity
return (1 - velocitySensitivity) * value + velocitySensitivity * self.velocity * value

def copy(self):
return MIDINote(self.channel, self.noteNumber, self.timeOn, self.timeOff, self.velocity)
19 changes: 13 additions & 6 deletions animation_nodes/data_structures/midi/midi_track.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,29 @@ class MIDITrack:
index: int = 0
notes: List[MIDINote] = field(default_factory = list)

def evaluate(self, time, channel, noteNumber, attackTime, attackInterpolation, releaseTime, releaseInterpolation):
def evaluate(self, time, channel, noteNumber,
attackTime, attackInterpolation, decayTime, decayInterpolation, sustainLevel,
releaseTime, releaseInterpolation, velocitySensitivity):

noteFilter = lambda note: note.channel == channel and note.noteNumber == noteNumber
timeFilter = lambda note: note.timeOff + releaseTime >= time >= note.timeOn
filteredNotes = filter(lambda note: noteFilter(note) and timeFilter(note), self.notes)
arguments = (time, attackTime, attackInterpolation, releaseTime, releaseInterpolation)
return max((note.evaluate(*arguments) for note in filteredNotes), default = 0)
arguments = (time, attackTime, attackInterpolation, decayTime, decayInterpolation,
sustainLevel, releaseTime, releaseInterpolation, velocitySensitivity)
return max((note.evaluate(*arguments) for note in filteredNotes), default = 0.0)

def evaluateAll(self, time, channel, attackTime, attackInterpolation, releaseTime, releaseInterpolation):
def evaluateAll(self, time, channel,
attackTime, attackInterpolation, decayTime, decayInterpolation, sustainLevel,
releaseTime, releaseInterpolation, velocitySensitivity):
channelFilter = lambda note: note.channel == channel
timeFilter = lambda note: note.timeOff + releaseTime >= time >= note.timeOn
filteredNotes = list(filter(lambda note: channelFilter(note) and timeFilter(note), self.notes))
arguments = (time, attackTime, attackInterpolation, releaseTime, releaseInterpolation)
arguments = (time, attackTime, attackInterpolation, decayTime, decayInterpolation,
sustainLevel, releaseTime, releaseInterpolation, velocitySensitivity)
noteValues = []
for i in range(128):
filteredByNumberNotes = filter(lambda note: note.noteNumber == i, filteredNotes)
value = max((note.evaluate(*arguments) for note in filteredByNumberNotes), default = 0)
value = max((note.evaluate(*arguments) for note in filteredByNumberNotes), default = 0.0)
noteValues.append(value)
return noteValues

Expand Down
11 changes: 9 additions & 2 deletions animation_nodes/nodes/sound/evaluate_midi_track.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,14 @@ def create(self):
self.newInput("Float", "Attack Time", "attackTime", value = 0.01)
self.newInput("Interpolation", "Attack Interpolation",
"attackInterpolation", defaultDrawType = "PROPERTY_ONLY")
self.newInput("Float", "Decay Time", "decayTime", value = 0.01)
self.newInput("Interpolation", "Decay Interpolation",
"decayInterpolation", defaultDrawType = "PROPERTY_ONLY")
self.newInput("Float", "Sustain Level", "sustainLevel", value = 0.6).setRange(0, 1)
self.newInput("Float", "Release Time", "releaseTime", value = 0.05)
self.newInput("Interpolation", "Release Interpolation",
"releaseInterpolation", defaultDrawType = "PROPERTY_ONLY")
self.newInput("Float", "Velocity Sensitivity", "velocitySensitivity", value = 0.0).setRange(0, 1)
self.newInput("Scene", "Scene", "scene", hide = True)

if self.evaluationType == "SINGLE":
Expand All @@ -40,7 +45,9 @@ def getExecutionCode(self, required):
yield "time = frame / AN.utils.scene.getFPS(scene)"
This conversation was marked as resolved.
Show resolved Hide resolved
if self.evaluationType == "SINGLE":
yield ("noteValue = track.evaluate(time, channel, noteNumber,"
"attackTime, attackInterpolation, releaseTime, releaseInterpolation)")
"attackTime, attackInterpolation, decayTime, decayInterpolation, min(max(sustainLevel, 0), 1), releaseTime, releaseInterpolation,"
"min(max(velocitySensitivity, 0), 1))")
else:
yield ("noteValues = DoubleList.fromValues(track.evaluateAll(time, channel,"
"attackTime, attackInterpolation, releaseTime, releaseInterpolation))")
"attackTime, attackInterpolation, decayTime, decayInterpolation, min(max(sustainLevel, 0), 1), releaseTime, releaseInterpolation,"
"min(max(velocitySensitivity, 0), 1)))")