Repository: geigr/ipytone (language: python)

Issue:
Leverage Tonejs Transport's Emitter implementation
Tonejs provides a signal interface via the `Emitter` class, which is implemented in `Transport`. It should be leveraged here for better syncing of ipytone widget attributes like `Transport.position` when transport signals are emitted, e.g., from within ipytone or from outside (e.g., [jupyterlab-daw](https://github.com/benbovy/jupyterlab-daw)).

It would be useful to also allow triggering custom Python callbacks when those signals are emitted.


Discussion / hints:

Fix patch:
diff --git a/docs/release_notes.rst b/docs/release_notes.rst
index f31fc61..b4a6f98 100644
--- a/docs/release_notes.rst
+++ b/docs/release_notes.rst
@@ -17,6 +17,8 @@ Enhancements
 - Added :py:meth:`Instrument.trigger_note` method that helps conditional
   scheduling of attack or release events within :py:class:`Part` callbacks.
   :py:class:`Note` has also a new ``trigger_type`` attribute (:pull:`105`).
+- Added ``Transport.state`` read-only attribute and use Tonejs' Transport
+  Emitter signals for syncing the state and position (:pull:`106`).
 
 Bug fixes
 ~~~~~~~~~
diff --git a/ipytone/transport.py b/ipytone/transport.py
index 5f4ed36..ce89caa 100644
--- a/ipytone/transport.py
+++ b/ipytone/transport.py
@@ -1,7 +1,7 @@
 import contextlib
 
 from ipywidgets import widget_serialization
-from traitlets import Bool, Float, Instance, Int, List, Unicode, Union
+from traitlets import Bool, Enum, Float, Instance, Int, List, Unicode, Union
 
 from .base import ToneObject
 from .callback import TimeCallbackArg, add_or_send_event
@@ -29,6 +29,13 @@ class Transport(ToneObject, ScheduleObserveMixin):
         help="transport time signature",
     ).tag(sync=True)
 
+    state = Enum(
+        ["started", "paused", "stopped"],
+        default_value="stopped",
+        read_only=True,
+        help="current playback state",
+    ).tag(sync=True)
+
     loop_start = Union(
         [Float(), Unicode()], default_value=0, help="starting position of transport loop"
     ).tag(sync=True)
diff --git a/src/widget_transport.ts b/src/widget_transport.ts
index 6cb09e2..1759b23 100644
--- a/src/widget_transport.ts
+++ b/src/widget_transport.ts
@@ -23,6 +23,7 @@ export class TransportModel extends ToneObjectModel implements ObservableModel {
       _model_name: TransportModel.model_name,
       _bpm: null,
       time_signature: 4,
+      state: 'stopped',
       loop_start: 0,
       loop_end: '4m',
       loop: false,
@@ -148,6 +149,11 @@ export class TransportModel extends ToneObjectModel implements ObservableModel {
     this.save_changes();
   }
 
+  private syncState(): void {
+    this.set('state', tone.Transport.state, { silent: true });
+    this.save_changes();
+  }
+
   private play(command: any): void {
     const argsArray = normalizeArguments(command.args, command.arg_keys);
     (tone.Transport as any)[command.method](...argsArray);
@@ -219,6 +225,22 @@ export class TransportModel extends ToneObjectModel implements ObservableModel {
       this.syncPosition();
     });
     this.on('msg:custom', this.handleMsg, this);
+
+    tone.Transport.on('start', () => {
+      this.syncState();
+    });
+    tone.Transport.on('pause', () => {
+      this.syncState();
+      this.syncPosition();
+    });
+    tone.Transport.on('stop', () => {
+      this.syncState();
+      this.syncPosition();
+    });
+    // TODO: this will be available in the next Tone.js release
+    // tone.Transport.on('ticks', () => {
+    //   this.syncPosition();
+    // });
   }
 
   static serializers: ISerializers = {


Test patch:
diff --git a/ipytone/tests/test_transport.py b/ipytone/tests/test_transport.py
index 9b622af..5296ef3 100644
--- a/ipytone/tests/test_transport.py
+++ b/ipytone/tests/test_transport.py
@@ -15,6 +15,8 @@ def test_transport():
     assert transport.bpm.value == 120
     assert transport.bpm.units == "bpm"
 
+    assert transport.state == "stopped"
+
     assert transport.time_signature == 4
 
     assert transport.loop is False