import importlib.util import math from pathlib import Path import numpy as np from numba import njit FROZEN_PATH = Path(__file__).resolve().parent.parent / 'guitar-fretting-05/model.py' SPEC = importlib.util.spec_from_file_location('frozen_fretted_geometry', FROZEN_PATH) FROZEN = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(FROZEN) PARAMETERS = dict(FROZEN.PARAMETERS) | { 'strings': 6, 'plectrumStiffnessNewtonsPerMeter': 1200., 'plectrumDampingNewtonSecondsPerMeter': .02, 'plectrumTravelMetersPerVelocity': .00036, 'plectrumHookDeflectionMeters': .000035, 'plectrumStrokeSeconds': .003, 'plectrumReleaseSeconds': .0015, 'fretPreparationSeconds': .004, 'unusedStringMuteRatePerSecond': 800., } def geometry(maximum_cell=None): shapes = [FROZEN.geometry(string, maximum_cell) for string in range(6)] if not all(np.array_equal(shape['positions'], shapes[0]['positions']) for shape in shapes): raise ValueError('String meshes must align for this solver') packed = {name: np.array([shape[name] for shape in shapes]) for name in ('masses', 'springs', 'pluckWeights', 'pickupWeights')} packed.update({name: shapes[0][name] for name in ('positions', 'fretNodes', 'bridgeMass', 'bridgeStiffness', 'bridgeDamping', 'bridgeWeights')}) packed['density'] = np.array([shape['density'] for shape in shapes]) packed['tension'] = np.array([shape['tension'] for shape in shapes]) return packed @njit(cache=True) def solve_bank_raw(diagonal, off_diagonal, bridge_diagonal, port_weights, coupling, pick_coefficient, pick_weights, rhs, bridge_rhs, pivots, reduced, free_solution, boundary_response, pick_response, output, bridge_output): strings, nodes = diagonal.shape bridge_load = 0. bridge_drive = 0. for string in range(strings): pivots[string, 0] = diagonal[string, 0] reduced[string, 0] = rhs[string, 0] if pick_coefficient[string] > 0: pick_response[string, 0] = pick_weights[string, 0] for node in range(1, nodes): multiplier = off_diagonal[string, node - 1] / pivots[string, node - 1] pivots[string, node] = diagonal[string, node] - multiplier * off_diagonal[string, node - 1] reduced[string, node] = rhs[string, node] - multiplier * reduced[string, node - 1] if pick_coefficient[string] > 0: pick_response[string, node] = pick_weights[string, node] - multiplier * pick_response[string, node - 1] if np.min(pivots[string]) <= 0: raise ValueError('Nonpositive string-bank solve pivot') free_solution[string, -1] = reduced[string, -1] / pivots[string, -1] boundary_response[string, -1] = 1 / pivots[string, -1] if pick_coefficient[string] > 0: pick_response[string, -1] /= pivots[string, -1] for node in range(nodes - 2, -1, -1): free_solution[string, node] = (reduced[string, node] - off_diagonal[string, node] * free_solution[string, node + 1]) / pivots[string, node] boundary_response[string, node] = -off_diagonal[string, node] * boundary_response[string, node + 1] / pivots[string, node] if pick_coefficient[string] > 0: pick_response[string, node] = (pick_response[string, node] - off_diagonal[string, node] * pick_response[string, node + 1]) / pivots[string, node] if pick_coefficient[string] > 0: compliance = 0. free_port = 0. boundary_port = 0. for node in range(nodes): compliance += pick_weights[string, node] * pick_response[string, node] free_port += pick_weights[string, node] * free_solution[string, node] boundary_port += pick_weights[string, node] * boundary_response[string, node] scale = pick_coefficient[string] / (1 + pick_coefficient[string] * compliance) for node in range(nodes): free_solution[string, node] -= scale * pick_response[string, node] * free_port boundary_response[string, node] -= scale * pick_response[string, node] * boundary_port bridge_load += coupling[string] - coupling[string] ** 2 * boundary_response[string, -1] bridge_drive += coupling[string] * free_solution[string, -1] compliance = 0. free_port = 0. for mode in range(len(port_weights)): compliance += port_weights[mode] ** 2 / bridge_diagonal[mode] free_port += port_weights[mode] * bridge_rhs[mode] / bridge_diagonal[mode] denominator = 1 + bridge_load * compliance if denominator <= 0: raise ValueError('Nonpositive shared bridge solve denominator') bridge_port = (free_port + compliance * bridge_drive) / denominator for mode in range(len(port_weights)): bridge_output[mode] = (bridge_rhs[mode] + port_weights[mode] * (bridge_drive - bridge_load * bridge_port)) / bridge_diagonal[mode] for string in range(strings): for node in range(nodes): output[string, node] = free_solution[string, node] + coupling[string] * boundary_response[string, node] * bridge_port @njit(cache=True) def solve_bank(diagonal, off_diagonal, bridge_diagonal, port_weights, coupling, pick_coefficient, pick_weights, rhs, bridge_rhs, pivots, reduced, free_solution, boundary_response, pick_response, output, bridge_output): solve_bank_raw(diagonal, off_diagonal, bridge_diagonal, port_weights, coupling, pick_coefficient, pick_weights, rhs, bridge_rhs, pivots, reduced, free_solution, boundary_response, pick_response, output, bridge_output) ratio = 0. for string in range(len(diagonal)): ratio = max(ratio, pick_coefficient[string] * np.sum(pick_weights[string] ** 2) / np.min(diagonal[string])) if ratio < 1e4: return residual = np.empty_like(rhs) bridge_residual = np.empty_like(bridge_rhs) correction = np.empty_like(output) bridge_correction = np.empty_like(bridge_output) for iteration in range(4): bridge_port = sum(port_weights * bridge_output) bridge_drive = 0. for string in range(len(diagonal)): pick_port = sum(pick_weights[string] * output[string]) for node in range(diagonal.shape[1]): value = diagonal[string, node] * output[string, node] if node: value += off_diagonal[string, node - 1] * output[string, node - 1] if node + 1 < diagonal.shape[1]: value += off_diagonal[string, node] * output[string, node + 1] value += pick_coefficient[string] * pick_weights[string, node] * pick_port if node == diagonal.shape[1] - 1: value -= coupling[string] * bridge_port residual[string, node] = rhs[string, node] - value bridge_drive += coupling[string] * output[string, -1] for mode in range(len(bridge_output)): value = bridge_diagonal[mode] * bridge_output[mode] value += port_weights[mode] * (np.sum(coupling) * bridge_port - bridge_drive) bridge_residual[mode] = bridge_rhs[mode] - value solve_bank_raw(diagonal, off_diagonal, bridge_diagonal, port_weights, coupling, pick_coefficient, pick_weights, residual, bridge_residual, pivots, reduced, free_solution, boundary_response, pick_response, correction, bridge_correction) output += correction bridge_output += bridge_correction @njit(cache=True) def accumulate(total, compensation, addition): corrected = addition - compensation updated = total + corrected return updated, (updated - total) - corrected @njit(cache=True) def smoothstep(value): value = min(1., max(0., value)) return value * value * (3 - 2 * value) @njit(cache=True) def energy(position, speed, bridge_position, bridge_speed, masses, springs, bridge_mass, bridge_stiffness, port_weights): total = 0. port = 0. for mode in range(len(port_weights)): port += port_weights[mode] * bridge_position[mode] total += .5 * bridge_mass[mode] * bridge_speed[mode] ** 2 total += .5 * bridge_stiffness[mode] * bridge_position[mode] ** 2 for string in range(len(position)): previous = 0. for node in range(position.shape[1]): total += .5 * masses[string, node] * speed[string, node] ** 2 total += .5 * springs[string, node] * (position[string, node] - previous) ** 2 previous = position[string, node] total += .5 * springs[string, -1] * (port - previous) ** 2 return total @njit(cache=True) def integrate(masses, springs, pick_weights, pickup_weights, bridge_mass, bridge_stiffness, bridge_damping, port_weights, fret_nodes, event_start, event_time, event_string, event_pitch, event_velocity, event_picked, event_gate, event_contacts, pick_order, open_pitches, internal_loss, mass_loss, rate, seconds, finger_stiffness, finger_ratio, hammer_seconds, pull_seconds, prepare_seconds, finger_travel, pick_stiffness, pick_damping, pick_travel, hook_deflection, stroke_seconds, release_seconds, mute_rate, loss_scale): strings, nodes = masses.shape modes = len(port_weights) frames = math.ceil(seconds * rate) time_step = 1 / rate half = time_step / 2 quarter = time_step ** 2 / 4 position = np.zeros((strings, nodes)) speed = np.zeros((strings, nodes)) bridge_position = np.zeros(modes) bridge_speed = np.zeros(modes) diagonal = np.empty((strings, nodes)) off_diagonal = np.empty((strings, nodes - 1)) rhs = np.empty((strings, nodes)) bridge_rhs = np.empty(modes) bridge_diagonal = bridge_mass + half * bridge_damping + quarter * bridge_stiffness coupling = np.empty(strings) pivots = np.empty((strings, nodes)) reduced = np.empty((strings, nodes)) free_solution = np.empty((strings, nodes)) boundary_response = np.empty((strings, nodes)) pick_response = np.empty((strings, nodes)) next_speed = np.empty((strings, nodes)) next_bridge_speed = np.empty(modes) fractions = np.zeros((strings, 24)) prior_fractions = np.zeros((strings, 24)) destination_fractions = np.zeros((strings, 24)) finger_target = np.zeros((strings, 24)) finger_velocity = np.zeros((strings, 24)) finger_damping = np.empty((strings, 24)) current_pitch = open_pitches.copy() old_pitch = open_pitches.copy() finger_begin = np.full(strings, -1.) finger_kind = np.zeros(strings, dtype=np.int64) finger_distance = np.zeros(strings) gate = np.zeros(strings) losses = np.empty(strings) pick_begin = np.full(strings, -1.) pick_release = np.full(strings, -1.) pick_origin = np.zeros(strings) pick_distance = np.zeros(strings) pick_fraction = np.zeros(strings) previous_pick_stiffness = np.zeros(strings) pick_target = np.zeros(strings) pick_velocity = np.zeros(strings) pick_coefficient = np.zeros(strings) output = np.empty((frames, strings)) stride = max(1, rate // 100) ledger = np.empty((2 + frames // stride, 11)) ledger[0] = np.zeros(11) samples = 1 finger_work = 0. pick_work = 0. dissipated = 0. finger_dissipated = 0. pick_dissipated = 0. bridge_exchange = 0. discharged_pick_energy = 0. maximum_balance_error = 0. maximum_pick_force = 0. compensation = np.zeros(7) pick_log = np.empty((len(event_time), 7)) picks_logged = 0 finger_event = 0 pick_event = 0 for string in range(strings): coupling[string] = springs[string, -1] * (quarter + half * internal_loss[string]) for fret in range(24): finger_damping[string, fret] = 2 * finger_ratio * math.sqrt(finger_stiffness * masses[string, fret_nodes[fret + 1]]) for node in range(nodes - 1): off_diagonal[string, node] = -(quarter + half * internal_loss[string]) * springs[string, node + 1] for frame in range(frames): time = (frame + .5) / rate step_finger_work = 0. step_pick_work = 0. step_dissipated = 0. step_finger_dissipated = 0. step_pick_dissipated = 0. step_bridge_exchange = 0. step_discharged = 0. while finger_event < len(event_start) and time >= event_start[finger_event]: string = event_string[finger_event] prior_fractions[string] = fractions[string] destination_fractions[string] = event_contacts[finger_event] old_pitch[string] = current_pitch[string] current_pitch[string] = event_pitch[finger_event] finger_begin[string] = event_start[finger_event] finger_kind[string] = 0 if event_picked[finger_event] else 1 if current_pitch[string] > old_pitch[string] else -1 finger_distance[string] = finger_travel * event_velocity[finger_event] gate[string] = event_gate[finger_event] finger_event += 1 for string in range(strings): duration = prepare_seconds if finger_kind[string] == 0 else hammer_seconds if finger_kind[string] == 1 else pull_seconds phase = (time - finger_begin[string]) / duration mix = smoothstep(phase) for fret in range(24): fractions[string, fret] = prior_fractions[string, fret] * (1 - mix) + destination_fractions[string, fret] * mix finger_target[string, fret] = 0. finger_velocity[string, fret] = 0. if finger_begin[string] >= 0 and phase < 1 and finger_kind[string] != 0: contact = current_pitch[string] - open_pitches[string] - 1 if finger_kind[string] == 1 else old_pitch[string] - open_pitches[string] - 1 if contact >= 0: if finger_kind[string] == 1: finger_target[string, contact] = finger_distance[string] * (1 - phase) finger_velocity[string, contact] = -finger_distance[string] / duration else: movement = min(1., phase / .6) finger_target[string, contact] = -finger_distance[string] * smoothstep(movement) if movement < 1: finger_velocity[string, contact] = -finger_distance[string] * 6 * movement * (1 - movement) / (.6 * duration) fractions[string, contact] = prior_fractions[string, contact] * (1 - smoothstep((phase - .6) / .4)) mute = min(1., max(0., (time - gate[string] + .024) / .024)) losses[string] = mass_loss + mute_rate * loss_scale * mute * mute while pick_event < len(event_time) and time >= event_time[pick_order[pick_event]]: selected_event = pick_order[pick_event] if event_picked[selected_event]: string = event_string[selected_event] pick_begin[string] = event_time[selected_event] pick_release[string] = -1. pick_origin[string] = 0. velocity_at_pick = 0. for node in range(nodes): pick_origin[string] += pick_weights[string, node] * position[string, node] velocity_at_pick += pick_weights[string, node] * speed[string, node] direction = 1. if picks_logged % 2 == 0 else -1. pick_distance[string] = direction * pick_travel * event_velocity[selected_event] pick_log[picks_logged] = np.array([time, float(string), direction, pick_origin[string], velocity_at_pick, pick_distance[string], energy(position, speed, bridge_position, bridge_speed, masses, springs, bridge_mass, bridge_stiffness, port_weights)]) picks_logged += 1 pick_event += 1 bridge_port = 0. bridge_velocity = 0. for mode in range(modes): bridge_port += port_weights[mode] * bridge_position[mode] bridge_velocity += port_weights[mode] * bridge_speed[mode] for string in range(strings): phase = (time - pick_begin[string]) / stroke_seconds pick_target[string] = pick_origin[string] + pick_distance[string] * smoothstep(phase) pick_velocity[string] = 0. if 0 <= phase < 1: pick_velocity[string] = pick_distance[string] * 6 * phase * (1 - phase) / stroke_seconds pick_position = 0. for node in range(nodes): pick_position += pick_weights[string, node] * position[string, node] if pick_begin[string] >= 0 and pick_release[string] < 0: if abs(pick_target[string] - pick_position) >= hook_deflection or phase >= 1: pick_release[string] = time fraction = 0. if pick_begin[string] >= 0: fraction = 1. if pick_release[string] >= 0: release_phase = min(1., (time - pick_release[string]) / release_seconds) fraction = .5 * (1 + math.cos(math.pi * release_phase)) pick_fraction[string] = fraction stiffness = pick_stiffness * fraction damping = pick_damping * fraction pick_coefficient[string] = quarter * stiffness + half * damping if stiffness < previous_pick_stiffness[string]: step_discharged += .5 * (previous_pick_stiffness[string] - stiffness) * (pick_position - pick_target[string]) ** 2 previous_pick_stiffness[string] = stiffness for node in range(nodes): left_position = position[string, node - 1] if node else 0. right_position = position[string, node + 1] if node + 1 < nodes else bridge_port left_velocity = speed[string, node - 1] if node else 0. right_velocity = speed[string, node + 1] if node + 1 < nodes else bridge_velocity spring_force = springs[string, node] * (position[string, node] - left_position) spring_force += springs[string, node + 1] * (position[string, node] - right_position) stiffness_speed = springs[string, node] * (speed[string, node] - left_velocity) stiffness_speed += springs[string, node + 1] * (speed[string, node] - right_velocity) viscous = 2 * losses[string] * masses[string, node] * speed[string, node] viscous += internal_loss[string] * stiffness_speed rhs[string, node] = masses[string, node] * speed[string, node] - half * viscous rhs[string, node] -= quarter * stiffness_speed + time_step * spring_force diagonal[string, node] = masses[string, node] * (1 + time_step * losses[string]) diagonal[string, node] += (quarter + half * internal_loss[string]) * (springs[string, node] + springs[string, node + 1]) for fret in range(24): node = fret_nodes[fret + 1] stiffness = finger_stiffness * fractions[string, fret] damping = finger_damping[string, fret] * fractions[string, fret] diagonal[string, node] += quarter * stiffness + half * damping rhs[string, node] -= (quarter * stiffness + half * damping) * speed[string, node] rhs[string, node] += time_step * (stiffness * (finger_target[string, fret] - position[string, node]) + damping * finger_velocity[string, fret]) pick_speed = 0. for node in range(nodes): pick_speed += pick_weights[string, node] * speed[string, node] forcing = time_step * (pick_stiffness * fraction * (pick_target[string] - pick_position) + pick_damping * fraction * pick_velocity[string]) forcing -= pick_coefficient[string] * pick_speed for node in range(nodes): rhs[string, node] += pick_weights[string, node] * forcing for mode in range(modes): spring_force = bridge_stiffness[mode] * bridge_position[mode] stiffness_speed = bridge_stiffness[mode] * bridge_speed[mode] viscous = bridge_damping[mode] * bridge_speed[mode] for string in range(strings): spring_force += port_weights[mode] * springs[string, -1] * (bridge_port - position[string, -1]) link_speed = port_weights[mode] * springs[string, -1] * (bridge_velocity - speed[string, -1]) stiffness_speed += link_speed viscous += internal_loss[string] * link_speed bridge_rhs[mode] = bridge_mass[mode] * bridge_speed[mode] - half * viscous bridge_rhs[mode] -= quarter * stiffness_speed + time_step * spring_force solve_bank(diagonal, off_diagonal, bridge_diagonal, port_weights, coupling, pick_coefficient, pick_weights, rhs, bridge_rhs, pivots, reduced, free_solution, boundary_response, pick_response, next_speed, next_bridge_speed) midpoint_bridge_velocity = 0. midpoint_bridge_position = 0. for mode in range(modes): midpoint_velocity = .5 * (bridge_speed[mode] + next_bridge_speed[mode]) midpoint_bridge_velocity += port_weights[mode] * midpoint_velocity midpoint_bridge_position += port_weights[mode] * (bridge_position[mode] + half * midpoint_velocity) step_dissipated += time_step * bridge_damping[mode] * midpoint_velocity ** 2 for string in range(strings): for fret in range(24): node = fret_nodes[fret + 1] midpoint_velocity = .5 * (speed[string, node] + next_speed[string, node]) midpoint_position = position[string, node] + half * midpoint_velocity stiffness = finger_stiffness * fractions[string, fret] damping = finger_damping[string, fret] * fractions[string, fret] force = stiffness * (finger_target[string, fret] - midpoint_position) force += damping * (finger_velocity[string, fret] - midpoint_velocity) step_finger_work += time_step * force * midpoint_velocity step_finger_dissipated += time_step * damping * (midpoint_velocity - finger_velocity[string, fret]) ** 2 midpoint_pick_position = 0. midpoint_pick_velocity = 0. for node in range(nodes): midpoint_velocity = .5 * (speed[string, node] + next_speed[string, node]) midpoint_pick_position += pick_weights[string, node] * (position[string, node] + half * midpoint_velocity) midpoint_pick_velocity += pick_weights[string, node] * midpoint_velocity stiffness = pick_stiffness * pick_fraction[string] damping = pick_damping * pick_fraction[string] force = stiffness * (pick_target[string] - midpoint_pick_position) force += damping * (pick_velocity[string] - midpoint_pick_velocity) maximum_pick_force = max(maximum_pick_force, abs(force)) step_pick_work += time_step * force * midpoint_pick_velocity step_pick_dissipated += time_step * damping * (midpoint_pick_velocity - pick_velocity[string]) ** 2 previous_midpoint_velocity = 0. for node in range(nodes): midpoint_velocity = .5 * (speed[string, node] + next_speed[string, node]) step_dissipated += time_step * 2 * losses[string] * masses[string, node] * midpoint_velocity ** 2 step_dissipated += time_step * internal_loss[string] * springs[string, node] * (midpoint_velocity - previous_midpoint_velocity) ** 2 previous_midpoint_velocity = midpoint_velocity position[string, node] += time_step * midpoint_velocity step_dissipated += time_step * internal_loss[string] * springs[string, -1] * (midpoint_bridge_velocity - previous_midpoint_velocity) ** 2 midpoint_last_position = position[string, -1] - half * previous_midpoint_velocity bridge_force = springs[string, -1] * (midpoint_last_position - midpoint_bridge_position) bridge_force += internal_loss[string] * springs[string, -1] * (previous_midpoint_velocity - midpoint_bridge_velocity) step_bridge_exchange += time_step * abs(bridge_force * midpoint_bridge_velocity) finger_work, compensation[0] = accumulate(finger_work, compensation[0], step_finger_work) pick_work, compensation[1] = accumulate(pick_work, compensation[1], step_pick_work) dissipated, compensation[2] = accumulate(dissipated, compensation[2], step_dissipated) finger_dissipated, compensation[3] = accumulate(finger_dissipated, compensation[3], step_finger_dissipated) pick_dissipated, compensation[4] = accumulate(pick_dissipated, compensation[4], step_pick_dissipated) bridge_exchange, compensation[5] = accumulate(bridge_exchange, compensation[5], step_bridge_exchange) discharged_pick_energy, compensation[6] = accumulate(discharged_pick_energy, compensation[6], step_discharged) for mode in range(modes): bridge_position[mode] += half * (bridge_speed[mode] + next_bridge_speed[mode]) speed[:] = next_speed bridge_speed[:] = next_bridge_speed bridge_readout = sum(port_weights * bridge_speed) for string in range(strings): signal = -bridge_readout for node in range(nodes): signal += pickup_weights[string, node] * speed[string, node] output[frame, string] = signal if frame % stride == stride - 1 or frame == frames - 1: stored = energy(position, speed, bridge_position, bridge_speed, masses, springs, bridge_mass, bridge_stiffness, port_weights) contact_storage = 0. for string in range(strings): pick_position = 0. for node in range(nodes): pick_position += pick_weights[string, node] * position[string, node] contact_storage += .5 * pick_stiffness * pick_fraction[string] * (pick_position - pick_target[string]) ** 2 for fret in range(24): contact_storage += .5 * finger_stiffness * fractions[string, fret] * (position[string, fret_nodes[fret + 1]] - finger_target[string, fret]) ** 2 residual = stored - finger_work - pick_work + dissipated maximum_balance_error = max(maximum_balance_error, abs(residual)) ledger[samples] = np.array([(frame + 1) / rate, stored, stored + contact_storage, finger_work, pick_work, dissipated, finger_dissipated, pick_dissipated, residual, bridge_exchange, discharged_pick_energy]) samples += 1 return output, ledger[:samples], pick_log[:picks_logged], maximum_balance_error, maximum_pick_force, position, speed, bridge_position, bridge_speed def simulate(events, seconds, *, sample_rate=None, maximum_cell=None, loss_scale=1., finger_motion=True): if not math.isfinite(seconds) or seconds <= 0 or not math.isfinite(loss_scale) or loss_scale < 0: raise ValueError('Invalid persistent instrument controls') if not events: raise ValueError('An instrument score is required') rows = [] for event in events: row = dict(event) string = row['string'] pitch = row['pitch'] if not isinstance(string, int) or not 0 <= string < 6: raise ValueError('Invalid assigned string') fret = pitch - PARAMETERS['openPitches'][string] if not isinstance(pitch, int) or not 0 <= fret <= 24: raise ValueError('Invalid fret pitch') if not math.isfinite(row['time']) or not 0 <= row['time'] < seconds: raise ValueError('Invalid event time') if not math.isfinite(row['velocity']) or row['velocity'] < 0: raise ValueError('Invalid stroke velocity') if not math.isfinite(row['gate']) or not row['time'] < row['gate'] <= seconds: raise ValueError('Invalid physical gate') preparation = list(set(row.get('preparePitches', [pitch]))) if len(preparation) > 4 or pitch not in preparation: raise ValueError('Invalid four-finger preparation') if any(not isinstance(value, int) or not 0 <= value - PARAMETERS['openPitches'][string] <= 24 for value in preparation): raise ValueError('Prepared fret is outside the assigned string') row['contacts'] = [1. if PARAMETERS['openPitches'][string] + fret + 1 in preparation and PARAMETERS['openPitches'][string] + fret + 1 <= pitch else 0. for fret in range(24)] row['start'] = row['time'] - PARAMETERS['fretPreparationSeconds'] if row['picked'] else row['time'] row['preparePitches'] = preparation rows.append(row) for string in range(6): assigned = sorted((row for row in rows if row['string'] == string), key=lambda row: row['time']) last_pick = -math.inf finger_available = -math.inf previous_pitch = PARAMETERS['openPitches'][string] for row in assigned: if row['start'] < finger_available: raise ValueError('Overlapping finger controls on the same string') duration = PARAMETERS['fretPreparationSeconds'] if row['picked'] else PARAMETERS['hammerSeconds'] if row['pitch'] > previous_pitch else PARAMETERS['pullSeconds'] finger_available = row['start'] + duration previous_pitch = row['pitch'] if row['picked']: if row['time'] - last_pick < PARAMETERS['plectrumStrokeSeconds'] + PARAMETERS['plectrumReleaseSeconds']: raise ValueError('Overlapping plectrum strokes on the same string') last_pick = row['time'] rows.sort(key=lambda row: row['start']) pick_order = np.argsort(np.array([row['time'] for row in rows]), kind='stable') shape = geometry(maximum_cell) rate = PARAMETERS['sampleRate'] if sample_rate is None else int(sample_rate) if rate < 44100 or rate % 44100: raise ValueError('Integration rate must be an oversampled multiple of 44100') internal = 2 * PARAMETERS['distributedLossMetersSquaredPerSecond'] * shape['density'] / shape['tension'] * loss_scale result = integrate(shape['masses'], shape['springs'], shape['pluckWeights'], shape['pickupWeights'], shape['bridgeMass'], shape['bridgeStiffness'], shape['bridgeDamping'] * loss_scale, shape['bridgeWeights'], shape['fretNodes'], np.array([row['start'] for row in rows]), np.array([row['time'] for row in rows]), np.array([row['string'] for row in rows], dtype=np.int64), np.array([row['pitch'] for row in rows], dtype=np.int64), np.array([row['velocity'] for row in rows]), np.array([row['picked'] for row in rows], dtype=np.bool_), np.array([row['gate'] for row in rows]), np.array([row['contacts'] for row in rows]), pick_order, np.array(PARAMETERS['openPitches'], dtype=np.int64), internal, math.log(1000) / PARAMETERS['fundamentalT60Seconds'] * loss_scale, rate, seconds, PARAMETERS['fingerStiffnessNewtonsPerMeter'], PARAMETERS['fingerDampingRatio'] * loss_scale, PARAMETERS['hammerSeconds'], PARAMETERS['pullSeconds'], PARAMETERS['fretPreparationSeconds'], PARAMETERS['pluckDisplacementMetersPerVelocity'] * PARAMETERS['fingerTravelFractionOfInitialPluck'] if finger_motion else 0., PARAMETERS['plectrumStiffnessNewtonsPerMeter'], PARAMETERS['plectrumDampingNewtonSecondsPerMeter'] * loss_scale, PARAMETERS['plectrumTravelMetersPerVelocity'], PARAMETERS['plectrumHookDeflectionMeters'], PARAMETERS['plectrumStrokeSeconds'], PARAMETERS['plectrumReleaseSeconds'], PARAMETERS['unusedStringMuteRatePerSecond'], loss_scale) signal, ledger, pick_log, residual, force, position, speed, bridge_position, bridge_speed = result if not np.isfinite(signal).all() or not np.isfinite(ledger).all(): raise FloatingPointError('Non-finite shared instrument state') scale = float(np.max(ledger[:, 1])) return { 'signalByString': signal * PARAMETERS['pickupGain'], 'signal': np.sum(signal, axis=1) * PARAMETERS['pickupGain'], 'sampleRate': rate, 'energy': ledger, 'picks': pick_log, 'maximumEnergyBalanceErrorJoules': residual, 'maximumRelativeEnergyBalanceError': residual / scale if scale else 0., 'maximumPlectrumForceNewtons': force, 'events': rows, 'finalStringPosition': position, 'finalStringVelocity': speed, 'finalBridgePosition': bridge_position, 'finalBridgeVelocity': bridge_speed, 'nodesPerString': len(shape['positions']), }