NSWindow should only be instantiated on the main thread

Hey I’m having this same problem on my M1 Mac running native Python 3.10.12 through conda, whenever I use fork or fork_unsynchronized I get the following error:

*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'NSWindow should only be instantiated on the main thread!'
*** First throw call stack:
(
        0   CoreFoundation                      0x000000018d2b48c0 __exceptionPreprocess + 176
        1   libobjc.A.dylib                     0x000000018cdadeb4 objc_exception_throw + 60
        2   CoreFoundation                      0x000000018d2d9bac _CFBundleGetValueForInfoKey + 0
        3   AppKit                              0x0000000190a29874 -[NSWindow _initContent:styleMask:backing:defer:contentView:] + 188
        4   AppKit                              0x0000000190a297ac -[NSWindow initWithContentRect:styleMask:backing:defer:] + 48
        5   libtk8.6.dylib                      0x00000001060e8230 TkMacOSXMakeRealWindowExist + 536
        6   libtk8.6.dylib                      0x00000001060e7e94 TkWmMapWindow + 76
        7   libtk8.6.dylib                      0x0000000106045238 MapFrame + 76
        8   libtcl8.6.dylib                     0x0000000105f66dec TclServiceIdle + 88
        9   libtcl8.6.dylib                     0x0000000105f471e0 Tcl_DoOneEvent + 120
        10  libtk8.6.dylib                      0x00000001060d8e80 TkpInit + 792
        11  libtk8.6.dylib                      0x000000010603dcd0 Initialize + 2424
        12  _tkinter.cpython-310-darwin.so      0x0000000102eee0a4 Tkapp_New + 876
        13  _tkinter.cpython-310-darwin.so      0x0000000102eedb18 _tkinter_create + 648
        14  python3.10                          0x000000010109f880 cfunction_vectorcall_FASTCALL + 200
        15  python3.10                          0x000000010113d540 call_function + 524
        16  python3.10                          0x0000000101138e60 _PyEval_EvalFrameDefault + 24940
        17  python3.10                          0x0000000101132364 _PyEval_Vector + 2036
        18  python3.10                          0x0000000101139658 _PyEval_EvalFrameDefault + 26980
        19  python3.10                          0x0000000101132364 _PyEval_Vector + 2036
        20  python3.10                          0x000000010104ec64 method_vectorcall + 172
        21  python3.10                          0x000000010113d540 call_function + 524
        22  python3.10                          0x0000000101138e60 _PyEval_EvalFrameDefault + 24940
        23  python3.10                          0x0000000101132364 _PyEval_Vector + 2036
        24  python3.10                          0x000000010104b850 _PyObject_FastCallDictTstate + 320
        25  python3.10                          0x000000010104c454 _PyObject_Call_Prepend + 176
        26  python3.10                          0x00000001010c3394 slot_tp_init + 116
        27  python3.10                          0x00000001010bbd64 type_call + 456
        28  python3.10                          0x000000010104b59c _PyObject_MakeTpCall + 612
        29  python3.10                          0x000000010113d5d8 call_function + 676
        30  python3.10                          0x00000001011393bc _PyEval_EvalFrameDefault + 26312
        31  python3.10                          0x0000000101132364 _PyEval_Vector + 2036
        32  python3.10                          0x0000000101139658 _PyEval_EvalFrameDefault + 26980
        33  python3.10                          0x0000000101132364 _PyEval_Vector + 2036
        34  python3.10                          0x0000000101139658 _PyEval_EvalFrameDefault + 26980
        35  python3.10                          0x0000000101132364 _PyEval_Vector + 2036
        36  python3.10                          0x0000000101139658 _PyEval_EvalFrameDefault + 26980
        37  python3.10                          0x0000000101132364 _PyEval_Vector + 2036
        38  python3.10                          0x000000010113d540 call_function + 524
        39  python3.10                          0x0000000101138e38 _PyEval_EvalFrameDefault + 24900
        40  python3.10                          0x0000000101132364 _PyEval_Vector + 2036
        41  python3.10                          0x000000010113d540 call_function + 524
        42  python3.10                          0x0000000101138e38 _PyEval_EvalFrameDefault + 24900
        43  python3.10                          0x0000000101132364 _PyEval_Vector + 2036
        44  python3.10                          0x000000010104ed10 method_vectorcall + 344
        45  python3.10                          0x0000000101204830 thread_run + 180
        46  python3.10                          0x00000001011a0230 pythread_wrapper + 48
        47  libsystem_pthread.dylib             0x000000018d163034 _pthread_start + 136
        48  libsystem_pthread.dylib             0x000000018d15de3c thread_start + 8
)
libc++abi: terminating due to uncaught exception of type NSException
zsh: abort      /Users/username/miniforge3/bin/python 
/Users/username/miniforge3/lib/python3.10/multiprocessing/resource_tracker.py:224: UserWarning: resource_tracker: There appear to be 2 leaked semaphore objects to clean up at shutdown                                                                     
  warnings.warn('resource_tracker: There appear to be %d '

I’m basically trying to run customtkinter through fork_unsynchronized, which works find on Intel Windows machines but it won’t work at all on my M1 Mac. It seems to be a problem with how the M1 handles threading and I’ve spent hours trying to find a fix but I’m not very advanced with Python and get lost in multiprocessing. It almost works when I run my scamp code in its own thread before starting the customtkinter GUI but my code runs on a while loop and when I start the scamp thread, it starts the while loop over and over until I run out of threads.

The issue you’re running into isn’t about “leaked semaphores”; it’s about trying to do GUI stuff on a forked thread I think, based on the error: 'NSWindow should only be instantiated on the main thread!'.

I’ve run into this kind of thing before; usually the solution is to do s = Session().run_as_server() and then fork any scamp stuff you want to do with s.fork. But if you can provide your code, or a simplified version with the same issue, I can advise better.

Ah, I see. I’ll try the run_as_server method but whenever I try to run my scamp loop in a fork or thread, it usually creates way too many threads and crashes. Here’s a simplified version of my code:

from scamp import *
from customtkinter import *
from time import sleep


class DrumMachine:
	def __init__(self):
		super().__init__()
		self.SOUNDFONT = "C:\\Users\\username\\Documents\\looper\\drumfont.sf2"
		self.TEMPO = 120
		self.NEW_TEMPO = 120

		self.SESSION = Session(tempo=self.TEMPO)
		self.DRUMMER = self.SESSION.new_part("Brush", soundfont=self.SOUNDFONT)

		self.BEAT = 1

		self.RIDE = 51
		self.SNARE = 27
		self.BASS = 36
		self.BRUSH = 40
	
	def loop_swirl(self):
		while True:
			self.DRUMMER.play_note(self.BRUSH, 1, 4)

	def start_loop(self):
		sleep(0.5)
		fork(self.loop_swirl)
		while self.BEAT < 5:
			whole_note = 4
			half_note = 2
			quarter_note = 1
			eighth_note = 0.5
			sixteenth_note = 0.25
			triplet_quarter_note = 0.3333333333333333
			triplet_eighth_note = 0.1666666666666667
			triplet_sixteenth_note = 0.0833333333333333
			if self.BEAT == 1:
				self.DRUMMER.play_chord([self.BASS, self.RIDE], 1, triplet_quarter_note * 2)
				self.DRUMMER.play_note(self.RIDE, 1, triplet_quarter_note)
			elif self.BEAT == 2:
				self.DRUMMER.play_chord([self.SNARE, self.RIDE], 1, triplet_quarter_note * 2)
				self.DRUMMER.play_note(self.RIDE, 1, triplet_quarter_note)
			elif self.BEAT == 3:
				self.DRUMMER.play_chord([self.BASS, self.RIDE], 1, triplet_quarter_note * 2)
				self.DRUMMER.play_note(self.RIDE, 1, triplet_quarter_note)
			elif self.BEAT == 4:
				self.DRUMMER.play_chord([self.SNARE, self.RIDE], 1, triplet_quarter_note * 2)
				self.DRUMMER.play_note(self.RIDE, 1, triplet_quarter_note)
			self.BEAT += 1
			if self.BEAT == 5:
				self.BEAT = 1
	
	def kill_loop(self):
		self.SESSION.kill()


class DrumGUI(CTk):
	def __init__(self, drum_machine):
		super().__init__()
		self.title("looper")
		self.geometry("800x600")
		self.font = CTkFont(family="Noto Sans Black", size=50)
		
		self.current_tempo = drum_machine.TEMPO
		self.tempo_label = CTkLabel(self, text=str(self.current_tempo) + " bpm", font=self.font)
		self.tempo_label.pack(padx=20, pady=(20, 10))

		self.tempo_slider = CTkSlider(self, width=600, from_= 60, to=200, command=self.on_click)
		self.tempo_slider.pack(padx=20, pady=(10, 20))
		self.tempo_slider.set(self.current_tempo)
		self.tempo_slider.bind("<ButtonRelease-1>", lambda update_tempo: self.update_tempo(0, drum_machine))

	def on_click(self, value):
		self.current_tempo = int(self.tempo_slider.get())
		self.tempo_label.configure(text=str(self.current_tempo) + " bpm")
	
	def update_tempo(self, value, drum_machine):
		if drum_machine.TEMPO != self.current_tempo:
			drum_machine.TEMPO = self.current_tempo
			drum_machine.SESSION.set_tempo_target(drum_machine.TEMPO, 4 - drum_machine.BEAT)


def start_app():
	looper = DrumMachine()

	def run_gui():
		app = DrumGUI(looper)
		app.protocol("WM_DELETE_WINDOW", looper.kill_loop)
		app.mainloop()
	
	fork_unsynchronized(run_gui)
	looper.start_loop()


start_app()

I’m not very experienced in Python and wanted to figure out classes better but it maybe seems like it would be better to have all of my code in a single class.

Thanks for taking the time to help me out!

FWIW, when I reworked your script to work with standard tkinter, it works on my linux machine:

from scamp import *
from tkinter import Tk, Scale
from tkinter.font import Font
from tkinter.ttk import Label
from time import sleep


class DrumMachine:
    def __init__(self):
        super().__init__()
        self.SOUNDFONT = "default"
        self.TEMPO = 120
        self.NEW_TEMPO = 120

        self.SESSION = Session(tempo=self.TEMPO)
        self.DRUMMER = self.SESSION.new_part("power", soundfont=self.SOUNDFONT)

        self.BEAT = 1

        self.RIDE = 51
        self.SNARE = 27
        self.BASS = 36
        self.BRUSH = 40
    
    def loop_swirl(self):
        while True:
            self.DRUMMER.play_note(self.BRUSH, 1, 4)

    def start_loop(self):
        sleep(0.5)
        fork(self.loop_swirl)
        while self.BEAT < 5:
            whole_note = 4
            half_note = 2
            quarter_note = 1
            eighth_note = 0.5
            sixteenth_note = 0.25
            triplet_quarter_note = 0.3333333333333333
            triplet_eighth_note = 0.1666666666666667
            triplet_sixteenth_note = 0.0833333333333333
            if self.BEAT == 1:
                self.DRUMMER.play_chord([self.BASS, self.RIDE], 1, triplet_quarter_note * 2)
                self.DRUMMER.play_note(self.RIDE, 1, triplet_quarter_note)
            elif self.BEAT == 2:
                self.DRUMMER.play_chord([self.SNARE, self.RIDE], 1, triplet_quarter_note * 2)
                self.DRUMMER.play_note(self.RIDE, 1, triplet_quarter_note)
            elif self.BEAT == 3:
                self.DRUMMER.play_chord([self.BASS, self.RIDE], 1, triplet_quarter_note * 2)
                self.DRUMMER.play_note(self.RIDE, 1, triplet_quarter_note)
            elif self.BEAT == 4:
                self.DRUMMER.play_chord([self.SNARE, self.RIDE], 1, triplet_quarter_note * 2)
                self.DRUMMER.play_note(self.RIDE, 1, triplet_quarter_note)
            self.BEAT += 1
            if self.BEAT == 5:
                self.BEAT = 1
    
    def kill_loop(self):
        self.SESSION.kill()


class DrumGUI(Tk):
    def __init__(self, drum_machine):
        super().__init__()
        self.title("looper")
        self.geometry("800x600")
        self.font = Font(family="Noto Sans Black", size=50)
        
        self.current_tempo = drum_machine.TEMPO
        self.tempo_label = Label(self, text=str(self.current_tempo) + " bpm", font=self.font)
        self.tempo_label.pack(padx=20, pady=(20, 10))

        self.tempo_slider = Scale(self, width=600, from_= 60, to=200, command=self.on_click)
        self.tempo_slider.pack(padx=20, pady=(10, 20))
        self.tempo_slider.set(self.current_tempo)
        self.tempo_slider.bind("<ButtonRelease-1>", lambda update_tempo: self.update_tempo(0, drum_machine))

    def on_click(self, value):
        self.current_tempo = int(self.tempo_slider.get())
        self.tempo_label.configure(text=str(self.current_tempo) + " bpm")
    
    def update_tempo(self, value, drum_machine):
        if drum_machine.TEMPO != self.current_tempo:
            drum_machine.TEMPO = self.current_tempo
            drum_machine.SESSION.set_tempo_target(drum_machine.TEMPO, 4 - drum_machine.BEAT)


def start_app():
    looper = DrumMachine()

    def run_gui():
        app = DrumGUI(looper)
        app.protocol("WM_DELETE_WINDOW", looper.kill_loop)
        app.mainloop()
    
    fork_unsynchronized(run_gui)
    looper.start_loop()


start_app()

However, I think that may not work for you on windows. If so, refactoring to put the gui on the main thread might make sense (note the run_as_server):

from scamp import *
from tkinter import Tk, Scale
from tkinter.font import Font
from tkinter.ttk import Label
from time import sleep


class DrumMachine:
    def __init__(self):
        super().__init__()
        self.SOUNDFONT = "default"
        self.TEMPO = 120
        self.NEW_TEMPO = 120

        self.SESSION = Session(tempo=self.TEMPO).run_as_server()
        self.DRUMMER = self.SESSION.new_part("power", soundfont=self.SOUNDFONT)

        self.BEAT = 1

        self.RIDE = 51
        self.SNARE = 27
        self.BASS = 36
        self.BRUSH = 40
    
    def loop_swirl(self):
        while True:
            self.DRUMMER.play_note(self.BRUSH, 1, 4)

    def start_loop(self):
        fork(self.loop_swirl)
        while self.BEAT < 5:
            whole_note = 4
            half_note = 2
            quarter_note = 1
            eighth_note = 0.5
            sixteenth_note = 0.25
            triplet_quarter_note = 0.3333333333333333
            triplet_eighth_note = 0.1666666666666667
            triplet_sixteenth_note = 0.0833333333333333
            if self.BEAT == 1:
                self.DRUMMER.play_chord([self.BASS, self.RIDE], 1, triplet_quarter_note * 2)
                self.DRUMMER.play_note(self.RIDE, 1, triplet_quarter_note)
            elif self.BEAT == 2:
                self.DRUMMER.play_chord([self.SNARE, self.RIDE], 1, triplet_quarter_note * 2)
                self.DRUMMER.play_note(self.RIDE, 1, triplet_quarter_note)
            elif self.BEAT == 3:
                self.DRUMMER.play_chord([self.BASS, self.RIDE], 1, triplet_quarter_note * 2)
                self.DRUMMER.play_note(self.RIDE, 1, triplet_quarter_note)
            elif self.BEAT == 4:
                self.DRUMMER.play_chord([self.SNARE, self.RIDE], 1, triplet_quarter_note * 2)
                self.DRUMMER.play_note(self.RIDE, 1, triplet_quarter_note)
            self.BEAT += 1
            if self.BEAT == 5:
                self.BEAT = 1
    
    def kill_loop(self):
        self.SESSION.kill()


class DrumGUI(Tk):
    def __init__(self, drum_machine):
        super().__init__()
        self.title("looper")
        self.geometry("800x600")
        self.font = Font(family="Noto Sans Black", size=50)
        
        self.current_tempo = drum_machine.TEMPO
        self.tempo_label = Label(self, text=str(self.current_tempo) + " bpm", font=self.font)
        self.tempo_label.pack(padx=20, pady=(20, 10))

        self.tempo_slider = Scale(self, width=600, from_= 60, to=200, command=self.on_click)
        self.tempo_slider.pack(padx=20, pady=(10, 20))
        self.tempo_slider.set(self.current_tempo)
        self.tempo_slider.bind("<ButtonRelease-1>", lambda update_tempo: self.update_tempo(0, drum_machine))

    def on_click(self, value):
        self.current_tempo = int(self.tempo_slider.get())
        self.tempo_label.configure(text=str(self.current_tempo) + " bpm")
    
    def update_tempo(self, value, drum_machine):
        if drum_machine.TEMPO != self.current_tempo:
            drum_machine.TEMPO = self.current_tempo
            drum_machine.SESSION.set_tempo_target(drum_machine.TEMPO, 4 - drum_machine.BEAT)


def start_app():
    looper = DrumMachine()

    def run_gui():
        app = DrumGUI(looper)
        app.protocol("WM_DELETE_WINDOW", looper.kill_loop)
        app.mainloop()
    
    looper.SESSION.fork(looper.start_loop)
    run_gui()
    

start_app()

Hope that helps!

(Also, if you want to support my work on SCAMP, consider joining my Patreon: Marc Evanstein | creating open-source tools and videos to help you make music in | Patreon. No pressure :slight_smile: )

1 Like

Thanks so much, Marc! Your run_as_server method works on my M1 Mac perfectly! My code was so close to working and I was exploring the wrong ways to fix it. :laughing:

1 Like