"""
Description: Note Harmonic Editor
Category: 
Shortcut: Ctrl+T
Level: Advanced
Version: 1.43
Copyright:	(c) Hit'n'Mix Ltd 2019-2020
Author:		Martin Dawe
License: 	Feel free to create an editable copy of this RipScript and base
			your own creations on it. Email to ripscripts@hitnmix.com and
			we will consider featuring it on the RipScripts page at
			hitnmix.com/ripscripts/
"""

import time
import math
import platform
from PIL import Image, ImageDraw, ImageTk
from tkinter import messagebox
import tkinter.font as TkFont

instructions_active= \
	(
	"Click & drag to paint with Sound Palette selection",
	"Click & drag to erase",
	"Click & drag to adjust level",
	"Click & drag to blend",
	"Ctrl+click example note, then click & drag to remove its sound",
	"Ctrl+click example note, then click & drag to remove everything but its sound",
	"Click & drag to remove noise spikes",
	"Click & drag to remove harmonic spikes",
	"Ctrl+click slice, then click & drag to clone its region",
	"Ctrl+click slice, then click & drag to reduce loudness to that of slice",
	"Ctrl+click slice, then click & drag to increase loudness to that of slice",
	"Ctrl+click slice, then click & drag to reduce loudness by that of slice",
	"Ctrl+click slice, then click & drag to increase loudness by that of slice")
instructions_inactive="\n\n"

selection_instructions_off = "Shift-click & drag to select & play region"
selection_instructions_on = "Press Del/Up/Down to adjust volume of selection. Shift-click to clear"
selection_instructions_inactive="\n\n"

free_message = "For more than 'Erase' & 'Remove Noise Spikes' edit modes, enter a free-trial or purchase activation code"

min_slice_width = 6
min_harmonic_height = 6

def ripscript():
	
	# Create class to provide update functionality from other RipScripts
	class NoteEditor():
		def __init__(self):
			return
		def note(self):
			# Return note currently being edited
			if not ripx.ripscript_running("Harmonic Editor"): return None
			if edit_note is not None and not edit_note: return None
			return edit_note
		def refresh(self, note=None):
			# Refresh display for note if it is the current one being edited
			if not ripx.ripscript_running("Harmonic Editor"): return
			nonlocal edit_note, anchor_slice
			if note is None: note = edit_note
			if edit_note is not None and edit_note and note is not None and note and edit_note == note:
				anchor_slice = 0
				clear_selection()
				canvas.xview('moveto', 0)
				update_layers()
				show_note(edit_note, None)
	sp = NoteEditor()
	# Store so accessible by other RipScripts
	ripx.harmonic_editor = sp
	
	# Ensure Sound Palette RipScript running
	if not ripx.ripscript_running(name="Sound Palette"):
		if not ripx.run_ripscript(name="Sound Palette"):
			ripx.pop_up("Sound Palette not found - please reinstall RipX")
			return
		
	# Load settings and set defaults
	settings = Settings()
	edit_modes = (
		"Paint", "Erase", "Adjust Level", "Smooth",
		"Remove Sound", "Keep Sound",
		"Remove Noise Spikes", "Flatten Harmonic Spikes",
		"Clone From Region",
		"Reduce to Slice", "Increase to Slice",
		"Subtract Slice", "Add Slice"
		)
	palette_mode = 0
	erase_mode = 1
	adjust_level_mode = 2
	blend_mode = 3
	remove_timbre_mode = 4
	keep_timbre_mode = 5
	remove_spikes_mode = 6
	remove_harmonics_mode = 7
	clone_from_slice_mode = 8
	reduce_to_slice_mode = 9
	increase_to_slice_mode = 10
	subtract_slice_mode = 11
	add_slice_mode = 12

	settings.add("edit_mode", edit_modes[erase_mode])
	settings.add("canvas_width", 512)
	settings.add("canvas_height", 384)
	settings.add("remove_to_other", 0)
	settings.add("time_scale", 1.0)
	settings.add("replace", 1.0)
	settings.add("level", 0.0)
	settings.add("harmonics_setting", "All")
	settings.add("brush_duration", 0.2)
	harmonics = range(1, MAX_HARMONIC_MULTIPLE + 1)

	free_message_shows = 0

	# Create window with canvas
	window = ripx.add_tk_window(sticky="nw", resizable=True)
	pad_x = 6 * pixel_scale()
	pad_y = 4 * pixel_scale()
	show_layers = False

	note_image = None
	canvas_image = None
	timbre_note: Note = None
	transfer_note: Note = None
	anchor_slice = 0
	edit_anchor_slice = 0
	edit_anchor_slice_start = edit_anchor_slice_end = 0
	options_frame = window.add_frame()
	options_frame.grid(row=0, column=0, sticky="we")
	options_frame.add_label(text="Mode").grid(row=0, column=0, sticky="e", padx=pad_x)
	edit_mode_combo = options_frame.add_combo_box(values=edit_modes, textvariable=edit_mode, width=30, height=13, state="readonly")
	edit_mode_combo.grid(row=0, column=1, pady=pad_y, sticky="we")
	def edit_mode_changed():
		nonlocal anchor_slice, free_message_shows
		mode = edit_mode_combo.current()
		if not in_slice_mode(): anchor_slice = 0
		update_slice_anchor()
		update_timbre_text()
		update_timbre_spotlight()
		if platform.system() == "Darwin": # Mac OS X
			instructions = instructions_active[edit_mode_combo.current()].replace("Ctrl+", "\u2318-")
		else:
			instructions = instructions_active[edit_mode_combo.current()]
		mode_instructions.config(text=instructions)
		update_transfer_button()
		# Replace/Level slider boxes present
		if mode == palette_mode or mode == erase_mode or mode == blend_mode:
			# Have Replace slider
			level_frame.grid_remove()
			replace_frame.grid(row=0, rowspan=2, column=4, sticky="w")
			if mode == palette_mode:
				replace_text_label['text'] = "Replace"
			elif mode == erase_mode:
				replace_text_label['text'] = "Amount"
			elif mode == blend_mode:
				replace_text_label['text'] = "Strength"
		elif mode == remove_spikes_mode or mode == remove_harmonics_mode:
			# Neither slider
			level_frame.grid_remove()
			replace_frame.grid_remove()
		else:
			# Level slider
			level_frame.grid(row=0, rowspan=2, column=4, sticky="w")
			replace_frame.grid_remove()
			
	def edit_mode_selected(event: Event):
		edit_mode_combo.selection_clear() # To prevent selected text that shouldn't apply to read-only combo
		edit_mode_changed()
	edit_mode_combo.bind("<<ComboboxSelected>>", edit_mode_selected)
	
	transfer_button = options_frame.add_check_button(text=" Remove To Another Note", variable=remove_to_other,
		tooltip="A new overlapping note is created if necessary and removed sound is transferred to it")
	transfer_button.grid(row=0, column=2, padx=pad_x, pady=pad_y, sticky="we")
	def transfer_possible():
		mode = edit_mode_combo.current()
		return mode == erase_mode or mode == adjust_level_mode or mode == remove_harmonics_mode or mode == remove_spikes_mode or mode == reduce_to_slice_mode or \
			mode == subtract_slice_mode or mode == remove_timbre_mode or mode == keep_timbre_mode
	def update_transfer_button():
		# Disable transfer button if not a transfer mode
		mode = edit_mode_combo.current()
		if transfer_possible():
			to_state = 'readonly'
		else:
			to_state = 'disabled'
		transfer_button.config(state=to_state)
		if to_state == 'disabled':
			transfer_button.grid_remove()
		else:
			transfer_button.grid(row=0, column=2, padx=pad_x, pady=pad_y, sticky="we")
	update_transfer_button()
			
	#nr_button = window.add_button(text=" Noise Removal... ",
	#	tooltip="Click here to open the Noise Removal tool to quickly remove foreground or background noise")
	#nr_button.grid(row=0, column=1, padx=pad_x, pady=pad_y, sticky="e")
	#def noise_removal(event: Event):
	#	ripx.run_ripscript(name="Noise Removal")	
	#nr_button.bind("<Button-1>", noise_removal)
		
	window.add_separator(orient=HORIZONTAL).grid(row=2, column=0, columnspan=2, sticky="we")
			
	def note_exists(note):
		if note is not None:
			if note:
				return True
			else:
				note = None
				return False
		else:
			return False
			
	if platform.system() == "Darwin": # Mac OS X
		instructions = instructions_active[edit_mode_combo.current()].replace("Ctrl+", "\u2318-")
	else:
		instructions = instructions_active[edit_mode_combo.current()]
	mode_instructions = window.add_label(text=instructions)
	mode_instructions.grid(row=3, column=0, columnspan=2, padx=pad_x, pady=pad_y, sticky="we")
	selection_instructions = window.add_label(text=selection_instructions_off)
	selection_instructions.grid(row=4, column=0, columnspan=2, padx=pad_x, sticky="we")
	window.add_separator(orient=HORIZONTAL).grid(row=5, column=0, columnspan=2, pady=pad_y, sticky="we")
	
	def db_adjustment():
		# Convert replace value to existing note volume adjustment
		return loudness(1.0 - replace.get()).db

	## Duration slider
	frame = window.add_frame()
	frame.grid(row=6, column=0, sticky="we")
	label = frame.add_label(text="Brush", anchor="w")
	label.grid(row=1, column=0, padx=pad_x, pady=0, sticky="w")
	def brush_duration_changed(loudness_str):
		brush_duration_label['text'] = " %.2f s" % brush_duration.get()
	brush_duration_scale = frame.add_scale(from_=0.01, to=1.0, orient=HORIZONTAL,
		variable=brush_duration, command=brush_duration_changed, length=100*pixel_scale(),
		tooltip="Drag slider to adjust duration of brush")
	brush_duration_scale.grid(row=0, column=0, columnspan=2, padx=pad_x, pady=0, sticky="w")
	brush_duration_label = frame.add_label(anchor="e", width=6)
	brush_duration_label.grid(row=1, column=1, padx=pad_x, pady=0, sticky="e")
	brush_duration_label['text'] = " %.2f s" % brush_duration.get()
		
	# Replace slider
	replace_frame = frame.add_frame()
	replace_frame.add_separator(orient=VERTICAL).grid(row=0, rowspan=2, column=3, sticky="ns", padx=pad_x)	
	replace_text_label = replace_frame.add_label(text="Replace", anchor="w")
	replace_text_label.grid(row=1, column=4, padx=pad_x, pady=0, sticky="w")
	def replace_changed(loudness_str):
		replace_label['text'] = " %.0f%%" % (replace.get() * 100)
	replace_scale = replace_frame.add_scale(from_=0.0, to=1.0, orient=HORIZONTAL,
		variable=replace, command=replace_changed, length=100*pixel_scale(),
		tooltip="Drag slider to adjust amount existing sound is affected")
	replace_scale.grid(row=0, column=4, columnspan=2, padx=pad_x, pady=5*pixel_scale(), sticky="w")
	replace_label = replace_frame.add_label(anchor="e", width=6)
	replace_label.grid(row=1, column=5, padx=pad_x, pady=0, sticky="e")
	replace_label['text'] = " %.0f%%" % (replace.get() * 100)
	
	# Level slider	
	level_frame = frame.add_frame()
	level_frame.add_separator(orient=VERTICAL).grid(row=0, rowspan=2, column=3, sticky="ns", padx=pad_x)	
	label = level_frame.add_label(text="Level", anchor="w")
	label.grid(row=1, column=4, padx=pad_x, pady=0, sticky="w")
	def level_changed(loudness_str):
		level_label['text'] = " %.0f dB" % level.get()
	level_scale = level_frame.add_scale(from_=-20.0, to=20.0, orient=HORIZONTAL,
		variable=level, command=level_changed, length=100*pixel_scale(),
		tooltip="Drag slider to adjust sound level")
	level_scale.grid(row=0, column=4, columnspan=2, padx=pad_x, pady=5*pixel_scale(), sticky="w")
	level_label = level_frame.add_label(anchor="e", width=6)
	level_label.grid(row=1, column=5, padx=pad_x, pady=0, sticky="e")
	level_label['text'] = " %.0f dB" % level.get()

	# Harmonics combo
	frame.add_separator(orient=VERTICAL).grid(row=0, rowspan=2, column=7, sticky="ns", padx=pad_x)
	label = frame.add_label(text="Harmonics")
	label.grid(row=1, column=8, padx=pad_x, sticky="w")
	harmonics_combo = frame.add_combo_box(values=["All", "Odd", "Even", "3rds", "5ths", "7ths"],
		width = 7, state="readonly", textvariable=harmonics_setting,
		tooltip="Select which harmonics to include")
	harmonics_combo.grid(row=0, column=8, padx=pad_x, pady=0, sticky="w")
	def harmonic_selected(event: Event):
		nonlocal harmonics
		harmonics_combo.selection_clear() # To prevent selected text that shouldn't apply to read-only combo
		if harmonics_setting.get() == "All":
			harmonics = list(range(1, MAX_HARMONIC_MULTIPLE + 1))
		elif harmonics_setting.get() == "Odd":
			harmonics = list(range(1, MAX_HARMONIC_MULTIPLE + 1, 2))
		elif harmonics_setting.get() == "Even":
			harmonics = list(range(2, MAX_HARMONIC_MULTIPLE + 1, 2))
		elif harmonics_setting.get() == "3rds":
			harmonics = list(range(3, MAX_HARMONIC_MULTIPLE + 1, 3))
		elif harmonics_setting.get() == "5ths":
			harmonics = list(range(5, MAX_HARMONIC_MULTIPLE + 1, 5))
		elif harmonics_setting.get() == "7ths":
			harmonics = list(range(7, MAX_HARMONIC_MULTIPLE + 1, 7))
	harmonics_combo.bind("<<ComboboxSelected>>", harmonic_selected)
	harmonic_selected(None)

	# Play/refresh buttons
	global play_image
	global refresh_image
	def get_image(filename):
		im = Image.open(ripscript_path(filename))
		scale = pixel_scale() / 3 # We are provided x2 sized bitmaps
		im = im.resize((int(im.width * scale), int(im.height * scale)), Image.ANTIALIAS)
		return ImageTk.PhotoImage(im)
	play_image=get_image("play_x2.png")
	stop_image=get_image("stop_x2.png")
	refresh_image=get_image("refresh_x2.png")

	button_frame = window.add_frame()
	button_frame.grid(row=6, column=1, sticky="e")
	play_button = button_frame.add_button(image=play_image, tooltip="Play (or press Spacebar)")
	play_button.grid(row=0, column=0, padx=0, pady=0, sticky="e")
	playback_rip = None
	playback_starttime = 0
	def play_update(event: Event=None):
		nonlocal playback_rip, playback_starttime
		# Still playing?
		# Remove cursor
		canvas.delete("playback_cursor")	
		playback_time = None
		if playback_rip and playback_rip is not None: playback_time = playback_rip.playback_time
		if playback_time is not None and edit_note is not None:
			# Update cursor
			tk_rgb_hl = "#%02x%02x%02x" % (0xff, 0xff, 0xff)	# Level 0 - bar level
			x = int((playback_time - playback_starttime) / edit_note.slice_duration + 0.5) * slice_width
			if x >= 0 and x < canvas.winfo_reqwidth() + canvas_hscoll_max:
				canvas.create_line(x, 16, x, canvas.winfo_reqheight(), fill=tk_rgb_hl, tags="playback_cursor")	
			window.after(40, play_update)
		else:
			# Stop
			play_button.config(image=play_image)
			
	def play(event: Event=None):
		nonlocal edit_note, anchor_slice, playback_rip, playback_starttime
		if event is not None and view.rip is not None and view.rip.playback_time is not None:
			view.rip.stop()
			return
		elif edit_note is not None:
			if selection_slice_range() is not None:
				play_selection()
			else:
				edit_note.play()
				playback_rip = view.rip
				playback_starttime = edit_note.start
			play_update()
			window.after(40, play_update)
			play_button.config(image=stop_image)

	def play_selection():
		nonlocal playback_rip, playback_starttime
		# Play selection
		# Create temporary RipCut with an empty note, then copy the slice/harmonic range to it and play it
		selected_slice_range = selection_slice_range()
		if selected_slice_range is not None:
			playback_ripcut = view.rip.new_ripcut(name="Note Playback")
			playback_note = playback_ripcut.add_note(at=0.0, percussion=edit_note.percussion)
			selected_slice_range.copy_to(note=playback_note)
			play_layer = visible_layer()
			if play_layer != 0: # Not all
				# Remove all but visible_layer
				## TODO: Update so can specify layers when creating slice range so not necessary
				for harmonic in playback_note.slices.harmonics:
					for layer in harmonic.layers:
						if layer.number != play_layer:
							layer.volume.amplitude = 0
			playback_note.play()
			playback_starttime = edit_note.start - selected_slice_range.start
			playback_rip = playback_ripcut # Playback ripcut becomes inaccessible after the following so can't currently track playback time
			playback_ripcut.remove()

	play_button.bind("<Button-1>", play)

	refresh_button = button_frame.add_button(image=refresh_image, tooltip="Refresh spectrum")
	refresh_button.grid(row=0, column=1, padx=pad_x, pady=0, sticky="e")
	def refresh_canvas(event: Event):
		nonlocal edit_note, anchor_slice
		if edit_note is not None:
			anchor_slice = 0
			clear_selection()
			canvas.xview('moveto', 0)
			update_layers()
			show_note(edit_note, None)
	refresh_button.bind("<Button-1>", refresh_canvas)

	## Canvas
	canvas_frame = window.add_frame()
	canvas_frame.grid(row=7, column=0, columnspan=2, padx=pad_x, pady=pad_y, sticky=(S,N,W,E))
	canvas_hscoll_max = canvas_width.get()
	canvas = canvas_frame.add_canvas(width=canvas_width.get(), height=canvas_height.get(), highlightthickness=0, scrollregion=(0,0,canvas_hscoll_max,288))
	canvas_hscroll = canvas_frame.add_scrollbar(orient=HORIZONTAL)
	canvas_hscroll.grid(row=1, column=0, sticky="we")
	canvas_hscroll.config(command=canvas.xview)
	canvas.config(xscrollcommand=canvas_hscroll.set)
	canvas.grid(row=0, column=0, sticky="nswe")
	#canvas_hscroll.grid_remove()
	canvas_after_id = 0
	def canvas_redraw():
		nonlocal note_image
		#if view.has_control:
		note_image = None
		show_note(edit_note, None)
	def canvas_resize(event: Event):
		# determine the ratio of old width/height to new width/height
		nonlocal harmonic_height, slice_width
		nonlocal canvas_after_id
		if not note_exists(edit_note):
			event.width = max(event.width, 48)
		else:
			event.width = max(event.width, edit_note.number_of_slices)
		event.height = max(event.height, MAX_HARMONIC_MULTIPLE)
		wscale = 1#float(event.width)/canvas_width.get()
		hscale = float(event.height)/canvas_height.get()
		canvas_width.set(event.width)
		canvas_height.set(event.height)
		
		# Hack to fix issue on Mac where dragging the top of the window doesn't move it and resizes the bottom only
		if platform.system() == "Darwin": # Mac OS X
			# Check if we're dragging the top, bottom, left, or right of the window
			mouse_x, mouse_y = [int(c) for c in window.winfo_pointerxy()]
			geometry_list = window.geometry().split('+')
			top = int(geometry_list[2])
			left = int(geometry_list[1])
			window_size = geometry_list[0].split('x')			
			bottom = int(window_size[1]) + top
			right = int(window_size[0]) + left
			distance_top = abs(top - mouse_y)
			distance_bottom = abs(bottom - mouse_y)
			distance_left = abs(left - mouse_x)
			distance_right = abs(right - mouse_x)
			if distance_top < distance_bottom:
				height_diff = int(canvas.config()['height'][4]) - event.height
				geometry_height = int(geometry_list[2]) + height_diff
				geometry_list[2] = str(geometry_height)
			if distance_left < distance_right:
				width_diff = int(canvas.config()['width'][4]) - event.width
				geometry_width = int(geometry_list[1]) + width_diff
				geometry_list[1] = str(geometry_width)
			if distance_top < distance_bottom or distance_left < distance_right:
				# Adjust window position to account for canvas resize
				window_geometry = '+'.join(geometry_list)
				window.geometry(window_geometry)
				
		# resize the canvas 
		canvas.config(width=canvas_width.get(), height=canvas_height.get())
		# rescale all the objects
		#canvas.scale("all",0,0,wscale,hscale)
		if note_exists(edit_note):
			# Size of each editable item
			harmonic_height = canvas.winfo_reqheight() / MAX_HARMONIC_MULTIPLE
			#slice_width = max(min_slice_width, canvas.winfo_reqwidth() / (edit_note.number_of_slices * time_scale.get()))
			# Add timer to redraw completely after a bit so looks correctly scaled with lines between harmonics
			if canvas_after_id: canvas.after_cancel(canvas_after_id)
			canvas_after_id = canvas.after(250, canvas_redraw)
	canvas.bind("<Configure>", canvas_resize)
	
	info_frame = window.add_frame()
	info_frame.grid(row=8, column=0, columnspan=2, pady=pad_y, sticky="we")
	global frequency_text, time_text, db_text
	db_text = info_frame.add_label(text="", width = 7)
	db_text.grid(row=0, column=2, padx=pad_x)
	frequency_text = info_frame.add_label(text="", width = 21)
	frequency_text.grid(row=0, column=3, padx=pad_x)
	time_text = info_frame.add_label(text="")
	time_text.grid(row=0, column=4, padx=pad_x)
	level_text = info_frame.add_label(text="")
	level_text.grid(row=0, column=5, padx=pad_x)
	
	label = info_frame.add_label(text="Zoom ", anchor="e")
	label.grid(row=0, column=0, sticky="e")
	current_time_scale = time_scale.get()
	def time_scale_changed(time_scale_str):
		nonlocal current_time_scale
		to_time_scale = time_scale.get()
		to_time_scale = (int)(to_time_scale * 2) / 2
		if to_time_scale == current_time_scale: return
		centre_x = canvas.winfo_reqwidth() / 2
		canvas.xview('moveto', (((canvas_hscroll.get()[0] * canvas_hscoll_max + centre_x) / current_time_scale) * to_time_scale - centre_x) / canvas_hscoll_max)
		current_time_scale = to_time_scale
		canvas_redraw()
	time_scale_slider = info_frame.add_scale(from_=1.0, to=4.0, orient=HORIZONTAL,
		variable=time_scale, command=time_scale_changed, length=100*pixel_scale(),
		tooltip="Drag slider to zoom in on time")
	time_scale_slider.grid(row=0, column=1, sticky="e")
	
	edit_note: Note = None
	def update_layers():
		if not show_layers: return
		nonlocal layers
		layers = [ "All" ]
		if layer_combo is not None:
			new_layer = max(0, layer_combo.current())
		else:
			new_layer = 0
		max_layers = 0
		if note_exists(edit_note):
			# Find max layers used
			for harmonic in edit_note.slices.harmonics:
				max_layers = max(max_layers, harmonic.number_of_layers)
			for layer in range(1, max_layers + 1):
				layers.append(str(layer))
		layer_combo['values'] = layers
		if new_layer > max_layers: new_layer = max_layers
		layer_combo.current(new_layer)
	def set_layer(event: Event):
		layer_combo.selection_clear() # To prevent selected text that shouldn't apply to read-only combo
		show_note(edit_note, None)
		play()
	if show_layers:
		layers = [ "All" ]
		layer_combo = info_frame.add_combo_box(values=layers, state="readonly", width=5)
		layer_combo.grid(row=0, column=3, sticky="w")
		label = info_frame.add_label(text="Visible Layer")
		label.grid(row=0, column=2, sticky="e")
		layer_combo.bind("<<ComboboxSelected>>", set_layer)
		update_layers()

	# Specify what is resizable (row 1, column 0)
	window.rowconfigure(7, weight=1)
	window.columnconfigure(0, weight=1)
	canvas_frame.rowconfigure(0, weight=1)
	canvas_frame.columnconfigure(0, weight=1)
	
	# Stop RipScript if window closed
	def window_closed(event):
		settings.save()
		view.unbind("<All>")
	window.bind("<Destroy>", window_closed)

	harmonic_height = 0
	slice_width = 0
	edit_note_volume = DB_SILENT
	source_note: Note = None
	last_edit_time = None
	last_edit_harmonic = None
	editing_note = False
	mix = TIMBRE | VOLUME
	effect_function = None
	canvas_spotlight = None
	canvas_spotlight_fade_in = None
	canvas_spotlight_fade_out = None
	selecting_region = False
	region_selected = False
	selection_harmonic_0 = selection_harmonic_1 = 0
	selection_slice_0 = selection_slice_1 = 0
	
	# Default font family
	font_family = TkFont.nametofont("TkDefaultFont").cget("family")

	def visible_layer():
		if show_layers:
			return layer_combo.current()
		else:
			return 0 # All

	# Function to add note details to canvas
	def show_note(note: Note, update_region):
		nonlocal canvas
		nonlocal harmonic_height
		nonlocal slice_width
		nonlocal canvas_hscoll_max
		nonlocal note_image, canvas_image, font_family
		
		force_black_blocks = update_region is not None
		update_beat_markers = False
		if update_region is None or not note_exists(note):
			# Clear all objects
			canvas.delete("all")
			update_beat_markers = True
			if note_image is not None:
				draw = ImageDraw.Draw(note_image)
				draw.rectangle((0, 0) + note_image.size, fill=(0, 0, 0, 0))
			if note is None or not note:
				#if view.has_control:
				canvas.create_rectangle(0, 0, canvas.winfo_reqwidth(), canvas.winfo_reqheight(), fill="#000000")
				tk_rgb = "#%02x%02x%02x" % (2 * 0x45, 2 * 0x61, 2 * 0x79)
				canvas.create_text(canvas.winfo_reqwidth() / 2 + canvas_hscroll.get()[0] * canvas_hscoll_max, canvas.winfo_reqheight() / 2,
					text="Click a Note with Move / Resize tool", fill=tk_rgb)
				return
			slice_width = max(min_slice_width, canvas.winfo_reqwidth() / note.number_of_slices) * time_scale.get()
			canvas_hscoll_max = slice_width*note.number_of_slices
			canvas.config(scrollregion=(0,0,canvas_hscoll_max,canvas_height.get()))
			#if canvas_hscoll_max <= canvas_width.get():
			#	canvas_hscroll.grid_remove()
			#else:
			#	canvas_hscroll.grid(row=1, column=0, sticky="we")

		if note_image is None:
			# Create PIL image
			note_image = Image.new(mode='RGB', color='black', size=(int(canvas_hscoll_max*pixel_scale()),int(canvas_height.get()*pixel_scale())))

		# Size of each editable item
		harmonic_height = canvas.winfo_reqheight() / MAX_HARMONIC_MULTIPLE
	
		if update_region is None:
			update_region = [ 1, note.number_of_slices + 1, range(1, note.number_of_harmonics + 1) ]
		else:
			# Clear objects in region
			for slice_position in range(update_region[0], update_region[1]):
				for harmonic_multiple in update_region[2]:
					tag = str(slice_position) + "_" + str(harmonic_multiple)
					canvas.delete(tag)

		# Layer
		show_layer = visible_layer()
		
		# If in note edit drag, pause so volume returns current loudness as opposed to pre-edit value
		if editing_note: note.edits_pause()
		
		# Add a rectangle for each
		x = (update_region[0] - 1) * slice_width
		draw = ImageDraw.Draw(note_image)
		for slice_position in range(update_region[0], update_region[1]):
			for harmonic_multiple in update_region[2]:
				harmonic = note.slices.harmonic(harmonic_multiple)
				
				total_loudness = 0
				if show_layer == 0:
					# All layers
					for layer in harmonic.layers:
						this_loudness = layer.slice(position=slice_position).volume.loudness
						total_loudness += this_loudness
				elif show_layer <= harmonic.number_of_layers:
					total_loudness += harmonic.layer(show_layer).slice(position=slice_position).volume.loudness
				
				if force_black_blocks or total_loudness > 0.003: # Faster with less rectangles especially on macOS
					tk_rgb = ripx.sound_palette.loudness_colour(total_loudness)
					y = canvas.winfo_reqheight() - harmonic_height * harmonic_multiple
					#tag = str(slice_position) + "_" + str(harmonic_multiple)
					# Old (Slow) style creating Canvas objects
					#canvas.create_rectangle(x, y, x + slice_width, y + harmonic_height - 2,
					#	fill=tk_rgb, outline=tk_rgb, tags=tag)
					draw.rectangle([(x, y), (x + slice_width, y + harmonic_height - 2)], fill=tk_rgb)
				
			x += slice_width
		
		# Add image
		canvas_image = ImageTk.PhotoImage(note_image)
		canvas.create_image(0, 0, image=canvas_image, anchor='nw')

		# Update beat markers
		canvas.delete("beat_marker")
		rip = view.rip
		start_bar = rip.bar_at(time=note.start).number
		end_bar = rip.bar_at(time=note.end).number
		y1 = canvas.winfo_reqheight()
		for bar_number in range(start_bar, end_bar + 1):
			bar = rip.bar(number=bar_number)
			bar_start = bar.start
			bar_duration = bar.duration
			# Draw visible bar markings
			
			beat_markers = bar.beat_markers(at_zoom=slice_width/2)
			for beat in beat_markers:
				if beat[1] == 0:
					tk_rgb_hl = "#%02x%02x%02x" % (0xff, 0xff, 0xff)	# Level 0 - bar level
				elif beat[1] == 1:
					tk_rgb_hl = "#%02x%02x%02x" % (0x80, 0x80, 0x80)	# Level 1 - top beat level
				else:
					tk_rgb_hl = "#%02x%02x%02x" % (0x40, 0x40, 0x40)	# Level 2 - subdivisions
				time = bar_start + beat[0] * bar_duration
				x = int((time - note.start) / note.slice_duration + 0.5) * slice_width
				if x >= 0 and x < canvas.winfo_reqwidth() + canvas_hscoll_max:
					canvas.create_line(x, 16, x, y1, fill=tk_rgb_hl, tags="beat_marker")
					#if beat[1] <= 1:
					canvas.create_text(x, 7, text=time_string(time), font=(font_family, 10), fill="#%02x%02x%02x" % (0xa0, 0xa0, 0xa0))
					

		# Resume edits if paused
		if editing_note: note.edits_start()
		
		# Re-add other graphics components
		update_slice_anchor()
		update_selection_marker()
		update_timbre_text()
	
	def view_mouse_click(event: ViewEvent):
		nonlocal edit_note, region_selected, anchor_slice
		if event.note and event.note is not None and (not edit_note or event.note != edit_note):
			anchor_slice = 0
			clear_selection()
			edit_note = event.note
			canvas.xview('moveto', 0)
			update_layers()
			show_note(edit_note, None)	
		return "to_ripx"
			
	def view_control_ended(event: ViewEvent):
		nonlocal region_selected, anchor_slice
		clear_selection()
		anchor_slice = 0
		canvas.delete("slice_anchor")
		show_note(None, None)
		tk_rgb = "#%02x%02x%02x" % (2 * 0x45, 2 * 0x61, 2 * 0x79)
		canvas.create_text(canvas.winfo_reqwidth() / 2 + canvas_hscroll.get()[0] * canvas_hscoll_max, canvas.winfo_reqheight() / 2,
			text="Click To Activate", fill=tk_rgb)
		#mode_instructions.config(text=instructions_inactive)
		if show_layers: layer_combo.config(state='disabled')
		canvas.config(scrollregion=(0,0,canvas_width.get(),canvas_height.get()))
		#canvas_hscroll.grid_remove()
			
	def view_undo(event: ViewEvent):
		# Undo/redo occurred - redisplay note
		#if view.has_control:
		update_layers()
		show_note(edit_note, None)
			
	# Create callback to show clicked note
	view = ripx.current_view
	view.bind("<Button-1>", view_mouse_click)
	#view.bind("<ControlEnded>", view_control_ended)
	view.bind("<Undo>", view_undo)
	
	def in_slice_mode():
		mode = edit_mode.get()
		return mode == edit_modes[reduce_to_slice_mode] or mode == edit_modes[increase_to_slice_mode] or \
			mode == edit_modes[add_slice_mode] or mode == edit_modes[subtract_slice_mode] or \
			mode == edit_modes[clone_from_slice_mode]
	
	def in_timbre_mode():
		mode = edit_mode.get()
		return mode == edit_modes[remove_timbre_mode] or mode == edit_modes[keep_timbre_mode]
		
	def adjust_And_transfer(layer_slice, transfer_layer_slice, loudness_adjust):
									
		if transfer_layer_slice is not None:
			# As in note edit drag, pause so volume returns current loudness as opposed to pre-edit value
			edit_note.edits_pause()
			original_amp = layer_slice.volume.amplitude
			edit_note.edits_start()
		
		# Update
		layer_slice.volume.loudness *= loudness_adjust
		
		if transfer_layer_slice is not None:
			# Get new amplitude and transfer difference to other note
			edit_note.edits_pause()
			new_amp = layer_slice.volume.amplitude
			edit_note.edits_start()	
			if transfer_layer_slice.volume.amplitude == 0:
				# Set up frequency and phase from original
				transfer_layer_slice.pitch.frequency = layer_slice.pitch.frequency
				for channel in CHANNELS:
					transfer_layer_slice.phase[channel] = layer_slice.phase[channel]
			transfer_layer_slice.volume.amplitude += original_amp - new_amp
			
	def level_for_mode():
		# Only Paint mode uses the Sound Palette's level as others nothing to do with that sound and use local slider
		if edit_mode.get() == edit_modes[palette_mode]:
			return float(ripx.sound_palette.volume.get())
		else:
			return float(level.get())

	def apply_edit(edit_time, edit_harmonic):

		# Mix audio from source_note (or nothing) to edit_note centred at time
						
		# Make local copies of fade so can adjust in case need to clip edit region to note
		this_fade_in = this_fade_out = min(0.05, brush_duration.get() / 4)
		this_duration = max(edit_note.slice_duration, float(brush_duration.get()))
		volume = level_for_mode()
		
		# How much to expand time and harmonics to cover any lag from last edit
		start_lag_time = 0
		end_lag_time = 0
		nonlocal last_edit_time
		if last_edit_time is not None:
			if edit_time > last_edit_time:
				start_lag_time = edit_time - last_edit_time
			else:
				end_lag_time = last_edit_time - edit_time
		last_edit_time = edit_time
		
		# Create list of harmonics to process since last edit
		nonlocal last_edit_harmonic
		harmonic_list = [edit_harmonic]
		min_edit_harmonic = max_edit_harmonic = edit_harmonic
		if last_edit_harmonic is not None:
			if edit_harmonic > last_edit_harmonic:
				for multiple in range(last_edit_harmonic, edit_harmonic):
					harmonic_list.append(multiple)
			else:
				for multiple in range(edit_harmonic + 1, last_edit_harmonic + 1):
					harmonic_list.append(multiple)
			min_edit_harmonic = min(last_edit_harmonic, edit_harmonic)
			max_edit_harmonic = max(last_edit_harmonic, edit_harmonic)
		last_edit_harmonic = edit_harmonic	
		# Remove any harmonics from list that are not valid multiples or one of
		# the requested harmonics from the Sound Palette
		harmonic_list = [multiple for multiple in harmonic_list
				if (multiple >= 1 and multiple <= MAX_HARMONIC_MULTIPLE and
					multiple in harmonics)]
		if not harmonic_list:
			return None
		
		edit_time_start = edit_time - start_lag_time - this_duration / 2
		# Clip to start of note and adjust this_fade_in accordingly
		unclipped_edit_time_start = edit_time_start
		if edit_time_start < 0:
			this_fade_in = max(0, this_fade_in + edit_time_start)
			edit_time_start = 0
			
		# Adjust to slice
		edit_time_start = edit_note.time_at(edit_note.slice_at(edit_time_start))
		edit_time_end = edit_time_start + this_duration + min(0, unclipped_edit_time_start + edit_note.slice_duration / 4)
		#edit_time_end = edit_note.time_at(edit_note.slice_at(edit_time_end))
		
		# Clip to end of note and adjust this_fade_out accordingly
		if edit_time_end > edit_note.duration:
			this_fade_out = max(0, this_fade_out - (edit_time_end - edit_note.duration))
			edit_time_end = edit_note.duration
		if edit_time_end <= edit_time_start:
			return None
		edit_slice_start = edit_note.slice_at(edit_time_start)
		edit_slice_end = edit_note.slice_at(edit_time_end) - 1
		
		# NB: Not used for palette_mode or erase_mode
		## TODO: Would need slice_range to support specifying layers
		edit_layer = visible_layer()
		
		# Set up transfer_slice to send removed amplitude to
		transfer_slice = None
		if transfer_note and (edit_mode.get() == edit_modes[palette_mode] or edit_mode.get() == edit_modes[erase_mode] or \
			edit_mode.get() == edit_modes[adjust_level_mode] or in_timbre_mode()):
			transfer_slice = transfer_note.slice_range(edit_time_start, min(transfer_note.end, edit_time_end), harmonic_list)

		if edit_mode.get() == edit_modes[palette_mode]:
						
			## Paint with Sound Palette Mode
			if source_note is not None:
				# Mix with source note
				source_time_start = edit_time - start_lag_time - this_duration / 2
				unclipped_source_time_start = source_time_start
				source_time_start = source_note.time_at(source_note.slice_at(max(0, source_time_start)))
				source_time_end = source_time_start + this_duration + min(0, unclipped_source_time_start + source_note.slice_duration / 4)
				
				edit_note.slice_range(edit_time_start, edit_time_end, harmonic_list).mix_in(
					source_note.slice_range(source_time_start, source_time_end),
					this_volume=edit_note_volume, volume=volume, fade_in=this_fade_in, fade_out=this_fade_out,
					transpose_by=0, mix=mix, effect=effect_function, remove_to=transfer_slice)
			else:
				# Mix with nothing - ie. lower amplitude or apply pure effect
				edit_note.slice_range(edit_time_start, edit_time_end, harmonic_list).mix_in(
					this_volume=edit_note_volume, fade_in=this_fade_in, fade_out=this_fade_out, remove_to=transfer_slice)
					
		elif edit_mode.get() == edit_modes[erase_mode]:
			## Erase Mode
			# Mix with nothing - ie. lower amplitude or apply pure effect
			edit_note.slice_range(edit_time_start, edit_time_end, harmonic_list).mix_in(
				this_volume=edit_note_volume, fade_in=this_fade_in, fade_out=this_fade_out, remove_to=transfer_slice)
					
		elif edit_mode.get() == edit_modes[adjust_level_mode]:
			## Adjust Level Mode
			# Mix with nothing - ie. lower/raise amplitude using level (rather than replacement)
			edit_note.slice_range(edit_time_start, edit_time_end, harmonic_list).mix_in(
				this_volume=volume, fade_in=this_fade_in, fade_out=this_fade_out, remove_to=transfer_slice)
				
		elif edit_mode.get() == edit_modes[blend_mode]:
			## Blend Mode
			# Set amplitude of each slice to overall amplitude of slices, using Sound Palette Replace value
			this_replace = replace.get()
			max_edit_harmonic = min(max_edit_harmonic, edit_note.number_of_harmonics)
			slice_range = edit_note.slice_range(edit_time_start, edit_time_end)
			for harmonic_multiple in range(min_edit_harmonic, max_edit_harmonic + 1):
				for layer in slice_range.harmonic(harmonic_multiple).layers:
					blended_amplitude = layer.volume.amplitude
					for slice_index in range(edit_slice_start, edit_slice_end + 1):
						original_amp = 0
						if this_replace < 1.0: original_amp = layer.slice(position=slice_index).volume.amplitude
						layer.slice(position=slice_index).volume.amplitude = blended_amplitude * this_replace + original_amp * (1 - this_replace)

		elif in_timbre_mode():
			## Remove/keep timbre from note
			keep_timbre = False
			if edit_mode.get() == edit_modes[keep_timbre_mode]: keep_timbre = True
			edit_note.slice_range(edit_time_start, edit_time_end, harmonic_list).remove_timbre(
				timbre_note.slices, volume=volume, fade_in=this_fade_in, fade_out=this_fade_out,
				remove_other=keep_timbre, remove_to=transfer_slice)
					
		elif edit_mode.get() == edit_modes[remove_harmonics_mode]:
			## Remove Harmonics Mode
			# For each slice look at harmonics above and below and reduce to the average
			if edit_harmonic <= edit_note.number_of_harmonics:
				transfer_amp = note_exists(transfer_note)
				slice_range = edit_note.slice_range(edit_time_start, edit_time_end)
				for slice_index in range(edit_slice_start, edit_slice_end + 1):
					loudness_this = loudness_above = loudness_below = 0
					for layer in slice_range.harmonic(edit_harmonic).layers:
						if edit_layer == 0 or layer.number == edit_layer:
							loudness_this += layer.slice(position=slice_index).volume.loudness
					if loudness_this > 0:
						if edit_harmonic > 1:
							for layer in slice_range.harmonic(edit_harmonic - 1).layers:
								if edit_layer == 0 or layer.number == edit_layer:
									loudness_below += layer.slice(position=slice_index).volume.loudness
						if edit_harmonic < edit_note.number_of_harmonics:
							for layer in slice_range.harmonic(edit_harmonic + 1).layers:
								if edit_layer == 0 or layer.number == edit_layer:
									loudness_above += layer.slice(position=slice_index).volume.loudness
						if edit_harmonic == 1:
							loudness_below = loudness_above
						loudness_adjust = (loudness_below + loudness_above) / 2 / loudness_this
						if loudness_adjust < 1.0:
											
							transfer_harmonic = None
							if transfer_amp: transfer_harmonic = transfer_note.slices.harmonic(edit_harmonic)

							for layer in slice_range.harmonic(edit_harmonic).layers:
								if edit_layer == 0 or layer.number == edit_layer:
									if transfer_harmonic is None:
										adjust_And_transfer(layer.slice(position=slice_index), None, loudness_adjust)
									else:
										adjust_And_transfer(layer.slice(position=slice_index), transfer_harmonic.layer(layer.number).slice(slice_index), loudness_adjust)
					
		elif edit_mode.get() == edit_modes[remove_spikes_mode]:
			## Remove Noise Spikes Mode
			# For each harmonic look at slices before and after and reduce to the average
			# If both sides of spotlight outside, use edges
			if edit_slice_start <= 1 and edit_slice_end > edit_note.number_of_slices - 1:
				edit_slice_start = 2
				edit_slice_end = edit_note.number_of_slices - 1
			slices_to_average = int(max(1, min(3, (edit_slice_end - edit_slice_start) / 3)))
			slice_range = edit_note.slices
			max_edit_harmonic = min(max_edit_harmonic, edit_note.number_of_harmonics)
			transfer_amp = note_exists(transfer_note)
			for harmonic_multiple in range(min_edit_harmonic, max_edit_harmonic + 1):
				loudness_before = loudness_after = 0
				
				if edit_slice_start > 1:
					slice_from = max(1, edit_slice_start - slices_to_average)
					for slice_index in range(slice_from, edit_slice_start):
						for layer in slice_range.harmonic(harmonic_multiple).layers:
							if edit_layer == 0 or layer.number == edit_layer:
								loudness_before += layer.slice(position=slice_index).volume.loudness
					loudness_before /= edit_slice_start - slice_from
					
				if edit_slice_end < edit_note.number_of_slices:
					slice_to = min(edit_note.number_of_slices + 1, edit_slice_end + slices_to_average + 1)
					for slice_index in range(edit_slice_end + 1, slice_to):
						for layer in slice_range.harmonic(harmonic_multiple).layers:
							if edit_layer == 0 or layer.number == edit_layer:
								loudness_after += layer.slice(position=slice_index).volume.loudness
					loudness_after /= slice_to - (edit_slice_end + 1)
					
				# If one side out, use the other
				if edit_slice_start <= 1: loudness_before = loudness_after
				if edit_slice_end >= edit_note.number_of_slices: loudness_after = loudness_before
				
				transfer_harmonic = None
				if transfer_amp: transfer_harmonic = transfer_note.slices.harmonic(harmonic_multiple)
					
				# Go through each slice within brush duration limiting loudness to
				# a gradient value between loudness_before and loudness_after
				weight_divisor = edit_slice_end - edit_slice_start + 2
				for slice_index in range(edit_slice_start, edit_slice_end + 1):
					loudness_this = 0
					for layer in slice_range.harmonic(harmonic_multiple).layers:
						if edit_layer == 0 or layer.number == edit_layer:
							loudness_this += layer.slice(position=slice_index).volume.loudness
					if loudness_this > 0:
						max_loudness = loudness_before * (edit_slice_end + 1 - slice_index) / weight_divisor + \
							loudness_after * (slice_index + 1 - edit_slice_start) / weight_divisor	
						loudness_adjust = max_loudness / loudness_this
						if loudness_adjust < 1.0:
							for layer in slice_range.harmonic(harmonic_multiple).layers:
								if edit_layer == 0 or layer.number == edit_layer:
									if transfer_harmonic is None:
										adjust_And_transfer(layer.slice(position=slice_index), None, loudness_adjust)
									else:
										adjust_And_transfer(layer.slice(position=slice_index), transfer_harmonic.layer(layer.number).slice(slice_index), loudness_adjust)
								
		elif in_slice_mode():
			if anchor_slice >= 1 and anchor_slice <= edit_note.number_of_slices:
				# Limit/Increase loudness of all slices to same harmonic at anchor_slice
				limit_loudness = edit_mode.get() == edit_modes[reduce_to_slice_mode] or edit_mode.get() == edit_modes[subtract_slice_mode]
				cloning = edit_mode.get() == edit_modes[clone_from_slice_mode]
				adding_subtracting = edit_mode.get() == edit_modes[add_slice_mode] or \
					edit_mode.get() == edit_modes[subtract_slice_mode] or cloning
				slice_range = edit_note.slices
				transfer_amp = limit_loudness and note_exists(transfer_note)
				if cloning:
					nonlocal edit_anchor_slice_start, edit_anchor_slice_end
					edit_anchor_slice_start = anchor_slice + edit_slice_start - edit_anchor_slice
					edit_anchor_slice_end = anchor_slice + edit_slice_end - edit_anchor_slice
				for harmonic_multiple in range(min_edit_harmonic, max_edit_harmonic + 1):
					# Get anchor slice loudness
					if not cloning:
						# Get fixed anchor loudness
						anchor_slice_loudness = 0
						for layer in slice_range.harmonic(harmonic_multiple).layers:
							if edit_layer == 0 or layer.number == edit_layer:
								anchor_slice_loudness += layer.slice(position=anchor_slice).volume.loudness
						anchor_slice_loudness *= db(volume).loudness	
									
					transfer_harmonic = None
					if transfer_amp: transfer_harmonic = transfer_note.slices.harmonic(harmonic_multiple)

					# Go through each slice to be affected by this
					for slice_index in range(edit_slice_start, edit_slice_end + 1):
						if cloning:
							# Get relative anchor loudness
							this_anchor_slice = anchor_slice + slice_index - edit_anchor_slice
							anchor_slice_loudness = 0
							if this_anchor_slice >= 1 and this_anchor_slice <= edit_note.number_of_slices:			
								for layer in slice_range.harmonic(harmonic_multiple).layers:
									if edit_layer == 0 or layer.number == edit_layer:
										anchor_slice_loudness += layer.slice(position=this_anchor_slice).volume.loudness
								anchor_slice_loudness *= db(volume).loudness
						loudness_this = 0
						for layer in slice_range.harmonic(harmonic_multiple).layers:
							if edit_layer == 0 or layer.number == edit_layer:
								loudness_this += layer.slice(position=slice_index).volume.loudness
						if loudness_this > 0:
							if adding_subtracting:
								if limit_loudness:
									loudness_adjust = max(0, loudness_this - anchor_slice_loudness) / loudness_this
								else:
									loudness_adjust = (loudness_this + anchor_slice_loudness) / loudness_this
							else:
								loudness_adjust = anchor_slice_loudness / loudness_this
							if (limit_loudness and loudness_adjust < 1.0) or (not limit_loudness and loudness_adjust > 1.0):
								for layer in slice_range.harmonic(harmonic_multiple).layers:
									if edit_layer == 0 or layer.number == edit_layer:
										if transfer_harmonic is None:
											adjust_And_transfer(layer.slice(position=slice_index), None, loudness_adjust)
										else:
											adjust_And_transfer(layer.slice(position=slice_index), transfer_harmonic.layer(layer.number).slice(slice_index), loudness_adjust)
											
						elif not limit_loudness:
							# If no amplitude exists, need to set all to first/visible layer
							layer = slice_range.harmonic(harmonic_multiple).layer(max(1, edit_layer))
							layer.slice(position=slice_index).volume.loudness = anchor_slice_loudness
							
		# Return the region updated so can update note display
		return [ edit_slice_start, edit_slice_end + 1, harmonic_list ]
		
	def harmonic_and_slice_for_event(event: Event):
		nonlocal slice_width
		if not note_exists(edit_note) or slice_width == 0: return 0,0
		nonlocal harmonic_height
		this_slice = 1 + int(round((event.x + canvas_hscroll.get()[0] * canvas_hscoll_max) / slice_width - 0.5))
		this_harmonic = 1 + int((canvas.winfo_reqheight() - event.y) / harmonic_height)
		this_slice = max(1, min(edit_note.number_of_slices, this_slice))
		this_harmonic = max(1, min(MAX_HARMONIC_MULTIPLE, this_harmonic))
		return this_harmonic, this_slice
		
	def selection_slice_range():
		# Return slice_range for selection
		if not note_exists(edit_note) or not region_selected: return None
		
		min_harmonic = max(1, min(selection_harmonic_0, selection_harmonic_1))
		max_harmonic = min(max(selection_harmonic_0, selection_harmonic_1), edit_note.number_of_harmonics) # Ensure we don't add new harmonics to edit_note
		if max_harmonic < min_harmonic: return None
		return edit_note.slice_range(
			start=edit_note.time_at(min(selection_slice_0, selection_slice_1)),
			end=edit_note.time_at(max(selection_slice_0, selection_slice_1) + 1),
			harmonics=range(min_harmonic, max_harmonic + 1))
			
	def update_slice_anchor():
		nonlocal anchor_slice
		canvas.delete("slice_anchor")
		if note_exists(edit_note) and in_slice_mode():
			if anchor_slice:
				anchor_slice_start = anchor_slice_end = anchor_slice
				if edit_mode.get() == edit_modes[clone_from_slice_mode]:
					if editing_note:
						anchor_slice_start = edit_anchor_slice_start
						anchor_slice_end = edit_anchor_slice_end
				canvas.create_rectangle(
					(anchor_slice_start - 1) * slice_width,
					0,
					anchor_slice_end * slice_width - 1,
					canvas.winfo_reqheight() - 1,
					outline="#00ff00", tags="slice_anchor")
			else:
				canvas.create_rectangle(0, canvas.winfo_reqheight() / 2 - 16, canvas.winfo_reqwidth(), canvas.winfo_reqheight() / 2 + 16,
					fill="#000000", tags="slice_anchor")
				tk_rgb = "#00ff00"
				if platform.system() == "Darwin": # Mac OS X
					canvas.create_text(canvas.winfo_reqwidth() / 2 + canvas_hscroll.get()[0] * canvas_hscoll_max, canvas.winfo_reqheight() / 2,
						text="\u2318-Click Here To Select Slice", fill=tk_rgb, tags="slice_anchor")
				else:
					canvas.create_text(canvas.winfo_reqwidth() / 2 + canvas_hscroll.get()[0] * canvas_hscoll_max, canvas.winfo_reqheight() / 2,
						text="Ctrl+Click Here To Select Slice", fill=tk_rgb, tags="slice_anchor")

	def update_timbre_text():
		canvas.delete("timbre_text")
		if note_exists(edit_note) and in_timbre_mode():
			if not note_exists(timbre_note):
				canvas.create_rectangle(0, canvas.winfo_reqheight() / 2 - 16, canvas.winfo_reqwidth(), canvas.winfo_reqheight() / 2 + 16,
					fill="#000000", tags="timbre_text")
				tk_rgb = "#00ff00"
				if platform.system() == "Darwin": # Mac OS X
					canvas.create_text(canvas.winfo_reqwidth() / 2 + canvas_hscroll.get()[0] * canvas_hscoll_max, canvas.winfo_reqheight() / 2,
						text="\u2318-Click Note In Rip", fill=tk_rgb, tags="timbre_text")
				else:
					canvas.create_text(canvas.winfo_reqwidth() / 2 + canvas_hscroll.get()[0] * canvas_hscoll_max, canvas.winfo_reqheight() / 2,
						text="Ctrl+Click Note In Rip", fill=tk_rgb, tags="timbre_text")

	def update_timbre_spotlight():
		if in_timbre_mode() and note_exists(timbre_note):
			duration = timbre_note.end-timbre_note.start
			view.spotlight(duration=duration, fade_in=duration/2, fade_out=duration/2, volume=40,
				source_note=timbre_note, source_time=timbre_note.start+duration/2)
		else:
			view.spotlight(source_note=None, source_time=-1)

	def update_selection_marker():
		canvas.delete("selection")
		if region_selected or selecting_region:
			tk_rgb = ripx.sound_palette.loudness_colour(0.25)
			canvas_spotlight = canvas.create_rectangle(
				(min(selection_slice_0, selection_slice_1) - 1) * slice_width,
				canvas.winfo_reqheight() - (min(selection_harmonic_0, selection_harmonic_1) - 1) * harmonic_height - 1,
				max(selection_slice_0, selection_slice_1) * slice_width - 1,
				canvas.winfo_reqheight() - (max(selection_harmonic_0, selection_harmonic_1)) * harmonic_height - 0,
				outline=tk_rgb, width=2, tags="selection")
				
	def time_string(seconds):
		# Return a string showing seconds as m:ss.ms
		ms = int(seconds * 100 % 100)
		ss = int(seconds % 60)
		m = int(seconds / 60)
		return "%d:%02d.%02d" % (m, ss, ms)
	
	def canvas_motion(event: Event):
		nonlocal edit_note
		nonlocal editing_note
		nonlocal canvas_spotlight
		nonlocal canvas_spotlight_fade_in
		nonlocal canvas_spotlight_fade_out
		nonlocal selection_harmonic_0, selection_slice_0, selection_harmonic_1, selection_slice_1, selecting_region

		#if not view.has_control: return
		
		if note_exists(edit_note) and slice_width > 0:
			this_harmonic, this_slice = harmonic_and_slice_for_event(event)
			time = edit_note.time_at(position=this_slice) + edit_note.slice_duration / 2
			
			# Update displayed frequency and time for harmonic/slice
			show_layer = max(1, visible_layer())
			if this_slice <= edit_note.number_of_slices:
				if this_harmonic <= edit_note.number_of_harmonics and show_layer <= edit_note.slices.harmonic(this_harmonic).number_of_layers:
					layer_slice = edit_note.slices.harmonic(this_harmonic).layer(show_layer).slice(this_slice)
					frequency_text.config(text="Harmonic %d (%.1f Hz)" % (this_harmonic, layer_slice.pitch.frequency))
					db_text.config(text="%d dB" % layer_slice.volume.db)
				else:
					frequency_text.config(text="Harmonic %d (-- Hz)" % this_harmonic)
					db_text.config(text="-- dB")
			else:
				frequency_text.config(text="Harmonic --")
				db_text.config(text="-- dB")
			if this_slice <= edit_note.number_of_slices:
				time_text.config(text="Slice %d (%s)" % (this_slice, time_string(edit_note.start + edit_note.time_at(this_slice))))
			else:
				time_text.config(text="Slice -- (-:--.--)")
		else:
			show_note(edit_note, None)
			return

		if editing_note:
			# Perform edit
			updated_region = apply_edit(time, this_harmonic)
			if updated_region is not None:
				# Show updated note
				show_note(edit_note, updated_region)
				
		elif selecting_region:
			# Draw selection marker
			selection_harmonic_1, selection_slice_1 = this_harmonic, this_slice
			update_selection_marker()
				
		elif edit_note is not None and edit_note:
			# Draw spotlight
			canvas.delete("spotlight")
						
			# Slice modes require slice to be set
			if in_slice_mode() and (anchor_slice < 1 or anchor_slice > edit_note.number_of_slices): return
						
			# Timbre modes require timbre note to be set
			if in_timbre_mode() and not note_exists(timbre_note):
				view.spotlight(source_note=None, source_time=-1)
				update_timbre_text()
				return

			using_palette = False
			if edit_mode.get() == edit_modes[palette_mode] or edit_mode.get() == edit_modes[erase_mode] or \
				edit_mode.get() == edit_modes[adjust_level_mode] or in_timbre_mode():
				using_palette = True
			
			this_duration = max(edit_note.slice_duration, float(brush_duration.get()))
			fade_in = fade_out = min(0.05, brush_duration.get() / 4)
			if using_palette:
				this_fade_in = min(this_duration / 2, fade_in)
				this_fade_out = min(this_duration / 2, fade_out)
			else:
				this_fade_in = this_fade_out = 0
			edit_time_start = max(0, time - this_duration / 2)
			# Adjust to slice
			edit_time_start = edit_note.time_at(edit_note.slice_at(edit_time_start))
			edit_time_end = edit_time_start + this_duration + min(0, time - this_duration / 2 + edit_note.slice_duration / 4)
			edit_time_start_fade = max(0, time - this_duration / 2 + this_fade_in)
			edit_time_end_fade = edit_time_end - this_fade_out
			edit_slice_start = edit_note.slice_at(edit_time_start)
			edit_slice_end = edit_note.slice_at(edit_time_end)
			edit_slice_start_fade = edit_note.slice_at(edit_time_start_fade)
			edit_slice_end_fade = edit_note.slice_at(edit_time_end_fade)
			pixel_start = (edit_slice_start - 1) * slice_width
			pixel_end = (edit_slice_end - 1) * slice_width
			pixel_start_fade = (edit_slice_start_fade - 1) * slice_width
			pixel_end_fade = (edit_slice_end_fade- 1) * slice_width
			pixel_top = canvas.winfo_reqheight() - (this_harmonic - 1) * harmonic_height - 1
			pixel_bot = pixel_top - harmonic_height + 1
			
			# Central full effect region
			if using_palette or in_slice_mode():
				level = db(level_for_mode()).loudness
				tk_rgb = ripx.sound_palette.loudness_colour(level / 8)
			else:
				tk_rgb = "#a0a0a0"
			canvas_spotlight = canvas.create_rectangle(
				pixel_start_fade,
				pixel_top - 1,
				pixel_end_fade - 1,
				pixel_bot,
				fill=tk_rgb, outline=tk_rgb, tags="spotlight")
			
			if using_palette:
				# Fade in/out edges
				for slice in range(edit_slice_start, edit_slice_start_fade):
					tk_rgb = ripx.sound_palette.loudness_colour(level / 8 * (slice - edit_slice_start + 1) / (edit_slice_start_fade - edit_slice_start + 1))
					x1 = (slice - 1) * slice_width
					x2 = (slice + 1 - 1) * slice_width
					canvas_spotlight_fade_in = canvas.create_rectangle(
						x1,
						pixel_top - 1,
						x2 - 1,
						pixel_bot,
						fill=tk_rgb, outline=tk_rgb, tags="spotlight")
				for slice in range(edit_slice_end_fade, edit_slice_end):
					tk_rgb = ripx.sound_palette.loudness_colour(level / 8 * (edit_slice_end - slice) / (edit_slice_end - edit_slice_end_fade + 1))
					x1 = (slice - 1) * slice_width
					x2 = (slice + 1 - 1) * slice_width
					canvas_spotlight_fade_in = canvas.create_rectangle(
						x1,
						pixel_top - 1,
						x2 - 1,
						pixel_bot,
						fill=tk_rgb, outline=tk_rgb, tags="spotlight")
					
	def canvas_leave(event: Event):
		if note_exists(edit_note):
			canvas.delete("spotlight")
			db_text.config(text="")
			frequency_text.config(text="")
			time_text.config(text="")
			
	def clear_selection():
		nonlocal region_selected
		region_selected = False
		canvas.delete("selection")
		selection_instructions.config(text=selection_instructions_off)
			
	def set_selection():
		nonlocal region_selected
		region_selected = True
		selection_instructions.config(text=selection_instructions_on)
		
	def setup_transfer_note():
		nonlocal transfer_note
		if transfer_possible() and remove_to_other.get() == 1:
			# Does a note already exist?
			transfer_note = None
			for note in view.rip.time_range(edit_note.start, edit_note.end).notes:
				if note != edit_note and note.start == edit_note.start and note.end == edit_note.end and note.pitch == edit_note.pitch:
					transfer_note = note
					break
			if not transfer_note:
				# Create silent copy of edit_note
				transfer_note = edit_note.copy_to()
				transfer_note.volume.amplitude = 0
				# Set to instrument name with (Removed) appended so easily selectable afterwards
				transfer_note.instrument = transfer_note.instrument + " (Removed)"
							
	def show_palette_settings():
		# Show current palette settings used by mode
		show_level = False
		mode = edit_mode_combo.current()
		if mode != blend_mode and mode != remove_spikes_mode and mode != remove_harmonics_mode and mode != erase_mode:
			show_level = True
		if show_level:
			level = round(level_for_mode())
			level_text.config(text="Level %.0f dB" % level)
			if level == 0:
				level_text.config(foreground='#ffffff')
			else:
				level_text.config(foreground='#ff8080')
		else:
			level_text.config(text="")
			
	def canvas_mouse_click(event: Event):
		nonlocal last_edit_time, last_edit_harmonic
		nonlocal editing_note
		nonlocal edit_note_volume
		nonlocal source_note
		nonlocal edit_note
		nonlocal region_selected
		nonlocal edit_anchor_slice
		
		# Ensure playback stopped
		if view.rip is not None: view.rip.stop()
		
		if note_exists(edit_note):
			
			# Slice modes require slice to be set
			if in_slice_mode() and (anchor_slice < 1 or anchor_slice > edit_note.number_of_slices): return
			
			# Timbre modes require timbre note to be set
			if in_timbre_mode():
				if not note_exists(timbre_note): return
				# Ensure it's not the same as the note being edited either
				if timbre_note == edit_note:
					ripx.pop_up("It is not possible to paint the selected timbre note with itself")
					return
					
			# Show current palette settings used by mode
			show_palette_settings()
					
			# Set up transfer note
			setup_transfer_note()
			
			# Edit slice anchor (for clone from slice mode)
			edit_anchor_harmonic, edit_anchor_slice = harmonic_and_slice_for_event(event)
				
			# Remember note and click time
			last_edit_time = None
			last_edit_harmonic = None
			
			# Set what to paint from Sound Palette (can be None)
			source_note = ripx.sound_palette.note(edit_note.pitch)
			edit_note_volume = db_adjustment()
			
			# Check both percussion or both harmonic
			if edit_mode.get() == edit_modes[palette_mode]:
				if source_note is None:
					ripx.pop_up("Sound Palette sound not found - please select another sound")
					return
				elif source_note.percussion != edit_note.percussion:
					ripx.pop_up("It is not possible to paint notes with unpitched sound, and vice versa")
					return
					
			# Start editing the clicked note
			edit_note.edits_start()
			editing_note = True
						
			
			# If selection rectangle shown, clear
			if region_selected:
				clear_selection()
			else:
				# Perform initial edit
				canvas.delete("spotlight")
				clear_selection()
				canvas_motion(event)
		else:
			show_note(edit_note, None)
			
	def canvas_mouse_shift_click(event: Event):
		#if not view.has_control: return
		nonlocal selection_harmonic_0, selection_slice_0, selection_harmonic_1, selection_slice_1, selecting_region, region_selected
		# Set up selection
		canvas.delete("spotlight")
		clear_selection()
		selecting_region = True
		selection_harmonic_0, selection_slice_0 = harmonic_and_slice_for_event(event)
		selection_harmonic_1, selection_slice_1 = 0, 0
				
	def canvas_mouse_ctrl_click(event: Event):
		#if not view.has_control: return
		nonlocal anchor_slice
		if in_slice_mode():
			# Set slice anchor
			anchor_harmonic, anchor_slice = harmonic_and_slice_for_event(event)
			update_slice_anchor()
			
	def end_edit():
		nonlocal editing_note
		edit_note.edits_end()
		editing_note = False
		update_slice_anchor()
		level_text.config(text="")
				
	def canvas_mouse_release(event: Event):
		nonlocal edit_note
		nonlocal editing_note
		nonlocal selection_harmonic_0, selection_slice_0, selection_harmonic_1, selection_slice_1, selecting_region, region_selected
		
		if not note_exists(edit_note): return
		
		if selecting_region:
			# If mouse not moved, select all to play
			if selection_harmonic_1 == 0:
				selecting_region = False
				clear_selection()
				return
				#selection_harmonic_0 = 1
				#selection_harmonic_1 = edit_note.number_of_harmonics
				#selection_slice_0 = 1
				#selection_slice_1 = edit_note.number_of_slices
				#update_selection_marker()
			selecting_region = False
			set_selection()
			# Play selection
			play()
		
		elif editing_note:
			# End edit on note
			end_edit()
			# Play note
			play()
			
	def canvas_esc_pressed(event: Event):
		nonlocal edit_note
		nonlocal editing_note
		
		if editing_note:
			# Cancel edit
			end_edit()
			return "break"

		# If not handled, must always signal to RipX to handle Esc presses
		return "to_ripx"
		
	def window_delete_pressed(event: Event):
		#if not view.has_control: return
		selected_slice_range = selection_slice_range()
		if selected_slice_range is not None:
			# Silence region
			selected_slice_range.volume.amplitude = 0.0
			show_note(edit_note, None)
			play()

	def window_updown_pressed(event: Event):
		#if not view.has_control: return
		selected_slice_range = selection_slice_range()
		if selected_slice_range is not None:
			# Adjust volume of region
			if event.keysym == "Up":
				selected_slice_range.volume += 2 # dB
			else:
				selected_slice_range.volume -= 2 # dB
			show_note(edit_note, None)
			play()
			return "break" # Prevent combo box itself getting event to show menu

	def window_selectall_pressed(event: Event):
		#if not view.has_control: return
		nonlocal selection_harmonic_0, selection_slice_0, selection_harmonic_1, selection_slice_1, selecting_region, region_selected
		if note_exists(edit_note):
			selection_harmonic_0 = 1
			selection_harmonic_1 = edit_note.number_of_harmonics
			selection_slice_0 = 1
			selection_slice_1 = edit_note.number_of_slices
			selecting_region = False
			set_selection()
			update_selection_marker()
				
	def mouse_ctrl_click(event: ViewEvent):
		#if not view.has_control: return
		nonlocal timbre_note
		if in_timbre_mode():
			if event.note is not None:
				# Remember note for using timbre
				timbre_note = event.note
				update_timbre_text()
				timbre_note.play()
				update_timbre_spotlight()
				return
		return "to_ripx"
		
	# Create callback to handle clicks on canvas
	tk_rgb = "#%02x%02x%02x" % (2 * 0x45, 2 * 0x61, 2 * 0x79)
	canvas.create_rectangle(0, 0, canvas.winfo_reqwidth(), canvas.winfo_reqheight(), fill="#000000")
	canvas.create_text(canvas.winfo_reqwidth() / 2 + canvas_hscroll.get()[0] * canvas_hscoll_max, canvas.winfo_reqheight() / 2,
		text="Please Wait", fill=tk_rgb)
	canvas.bind("<Button-1>", canvas_mouse_click)
	canvas.bind("<Shift-Button>", canvas_mouse_shift_click)
	if platform.system() == "Darwin": # Mac OS X
		canvas.bind("<Command-Button>", canvas_mouse_ctrl_click)
	else:
		canvas.bind("<Control-Button>", canvas_mouse_ctrl_click)
	canvas.bind("<Motion>", canvas_motion)
	canvas.bind("<Leave>", canvas_leave)
	canvas.bind("<ButtonRelease-1>", canvas_mouse_release)
	window.bind("<Key-BackSpace>", window_delete_pressed)
	window.bind("<Key-Delete>", window_delete_pressed)
	window.bind("<Key-Up>", window_updown_pressed)
	window.bind("<Key-Down>", window_updown_pressed)
	edit_mode_combo.bind("<Key-Up>", window_updown_pressed)
	edit_mode_combo.bind("<Key-Down>", window_updown_pressed)
	window.bind("<Control-a>", window_selectall_pressed)
	window.bind("<space>", play)
	# If we want Esc to be processed to enable undo we can bind to view
	# as Escape key presses get passed down from the tkinter window
	view.bind("<Key-Escape>", canvas_esc_pressed)
	view.bind("<Control-Button>", mouse_ctrl_click)

	# If one note selected currently, show in editor
	if view.rip is not None and len(view.rip.selected_notes) == 1:
		# Requires Sound Palette to have already started
		if hasattr(ripx, "sound_palette"):
			for edit_note in view.rip.selected_notes:
				break
	#view.take_control()
	update_layers()
	view.spotlight(source_note=None)
	if edit_note is None: show_note(None, None)
	edit_mode_changed()
	# Set main HM editor mode to regular editing mode since less confusing clicking on notes
	view.set_edit_mode(mode=MODE_MOVE_RESIZE)
