Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Right clicking on spectrogram or pitch graph sets playback position #286

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions friture/Plot.qml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Rectangle {
required property ScopeData scopedata
default property alias content: plotItemPlaceholder.children

signal pointSelected(real x, real y)

GridLayout {
anchors.fill: parent
rowSpacing: 2
Expand Down Expand Up @@ -68,6 +70,8 @@ Rectangle {
id: plotItemPlaceholder
anchors.fill: parent
}

onPointSelected: (x, y) => plot.pointSelected(x, y)
}

Item {
Expand Down
16 changes: 15 additions & 1 deletion friture/PlotArea.qml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ Item {

default property alias content: plotItemPlaceholder.children

signal pointSelected(real x, real y)

PlotBackground {
anchors.fill: parent
}
Expand Down Expand Up @@ -40,7 +42,7 @@ Item {
Item
{
id: crosshair
visible: plotMouseArea.pressed
visible: plotMouseArea.pressed && plotMouseArea.pressedButtons & Qt.LeftButton
anchors.fill: parent

property double posX: Math.min(Math.max(plotMouseArea.mouseX, 0), scopePlotArea.width)
Expand Down Expand Up @@ -83,6 +85,18 @@ Item {
MouseArea {
id: plotMouseArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
cursorShape: Qt.CrossCursor

onClicked: (event) => {
if (event.button == Qt.RightButton) {
scopePlotArea.pointSelected(
scopePlotArea.horizontal_axis.coordinate_transform.toPlot(
crosshair.relativePosX),
scopePlotArea.vertical_axis.coordinate_transform.toPlot(
1. - crosshair.relativePosY)
);
}
}
}
}
4 changes: 4 additions & 0 deletions friture/Scope.qml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Item {
id: container
property var stateId

signal pointSelected(real x, real y)

// delay the load of the Plot until stateId has been set
Loader {
id: loader
Expand All @@ -34,6 +36,8 @@ Item {
curve: modelData
}
}

onPointSelected: (x, y) => container.pointSelected(x, y)
}
}
}
6 changes: 6 additions & 0 deletions friture/dock.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
if TYPE_CHECKING:
from friture.analyzer import Friture
from friture.dockmanager import DockManager
from friture.playback.control import PlaybackControlWidget
from PyQt5.QtQml import QQmlEngine


Expand All @@ -43,6 +44,7 @@ def __init__(

self.dockmanager: 'DockManager' = parent.dockmanager
self.audiobuffer = parent.audiobuffer
self.playback_widget: 'PlaybackControlWidget' = parent.playback_widget

self.setObjectName(name)

Expand Down Expand Up @@ -109,6 +111,10 @@ def widget_select(self, widgetId: int) -> None:
self.audiowidget = constructor(self)
assert self.audiowidget is not None # mypy can't prove this :(

if hasattr(self.audiowidget, 'connect_time_selected'):
self.audiowidget.connect_time_selected(
self.playback_widget.on_time_selected)

# audiowidget is duck typed for this:
self.audiowidget.set_buffer(self.audiobuffer) # type: ignore
self.audiobuffer.new_data_available.connect(
Expand Down
12 changes: 11 additions & 1 deletion friture/pitch_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
from PyQt5.QtQuick import QQuickWindow
from PyQt5.QtQuickWidgets import QQuickWidget
from PyQt5.QtQml import QQmlComponent, QQmlEngine
from typing import Any, Optional
from typing import Any, Callable, Optional

from friture.audiobackend import SAMPLING_RATE
from friture.audiobuffer import AudioBuffer
Expand Down Expand Up @@ -67,6 +67,9 @@ def format_frequency(freq: float) -> str:


class PitchTrackerWidget(QtWidgets.QWidget):
# x=time is negative from present, y=frequency in Hz
point_selected = pyqtSignal(float, float)

def __init__(self, parent: QtWidgets.QWidget, engine: QQmlEngine):
super().__init__(parent)

Expand Down Expand Up @@ -109,6 +112,7 @@ def __init__(self, parent: QtWidgets.QWidget, engine: QQmlEngine):

root: Any = self.quickWidget.rootObject()
root.setProperty("stateId", state_id)
root.pointSelected.connect(self.on_point_selected)

self.gridLayout.addWidget(self.quickWidget, 0, 0, 1, 1)

Expand Down Expand Up @@ -173,6 +177,12 @@ def on_status_changed(self, status: QQuickWidget.Status) -> None:
for error in self.quickWidget.errors():
self.logger.error("QML error: " + error.toString())

def on_point_selected(self, x: float, y: float) -> None:
self.point_selected.emit(x, y)

def connect_time_selected(self, slot: Callable[[float], None]) -> None:
self.point_selected.connect(lambda t, _f: slot(t))

# method
def canvasUpdate(self) -> None:
# nothing to do here
Expand Down
8 changes: 8 additions & 0 deletions friture/playback/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,16 @@ def on_playback_stopped(self) -> None:
self.root.setPlaybackPosition(self.player.play_start_time)

def on_playback_position_changed(self, value: float) -> None:
# This handles changes in the slider
self.player.play_start_time = value

def on_time_selected(self, time: float) -> None:
# This handles clicks on plot widgets, i.e. the slider also needs
# to be updated.
time = max(time, -self.player.recorded_len_sec)
self.root.setPlaybackPosition(time)
self.on_playback_position_changed(time)

def on_recorded_len_changed(self, length: float) -> None:
# Always give the slider a nonzero length even if nothing is recorded
self.root.setRecordingStartTime(-max(length, 0.1))
Expand Down
6 changes: 5 additions & 1 deletion friture/playback/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,11 @@ def handle_new_data(self, data: np.ndarray) -> None:
self.recorded_len + data.shape[1], self.history_samples)
if new_len != self.recorded_len:
self.recorded_len = new_len
self.recorded_length_changed.emit(self.recorded_len / SAMPLING_RATE)
self.recorded_length_changed.emit(self.recorded_len_sec)

@property
def recorded_len_sec(self) -> float:
return self.recorded_len / SAMPLING_RATE

def play(self) -> None:
if self.state != PlayState.STOPPED:
Expand Down
6 changes: 6 additions & 0 deletions friture/plotting/canvasWidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
class CanvasWidget(QtWidgets.QWidget):

resized = QtCore.pyqtSignal(int, int)
point_selected = QtCore.pyqtSignal(float, float)

def __init__(self, parent, verticalScaleTransform, horizontalScaleTransform):
super(CanvasWidget, self).__init__(parent)
Expand Down Expand Up @@ -169,6 +170,11 @@ def mouseReleaseEvent(self, event):
self.ruler = False
# ask for update so the the ruler is actually erased
self.update()
if event.button() == QtCore.Qt.RightButton:
self.point_selected.emit(
self.horizontalScaleTransform.toPlot(event.x()),
self.verticalScaleTransform.toPlot(float(self.height() - event.y()))
)

def mouseMoveEvent(self, event):
if event.buttons() & QtCore.Qt.LeftButton:
Expand Down
14 changes: 14 additions & 0 deletions friture/spectrogram.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@
"""Spectrogram widget, that displays a rolling 2D image of the time-frequency spectrum."""

from PyQt5 import QtWidgets
import PyQt5.QtCore as QtCore
from numpy import log10, floor, zeros, float64, tile, array
from typing import Callable

from friture.imageplot import ImagePlot
from friture.audioproc import audioproc # audio processing class
from friture.spectrogram_settings import (Spectrogram_Settings_Dialog, # settings dialog
Expand All @@ -39,6 +42,8 @@


class Spectrogram_Widget(QtWidgets.QWidget):
# x=time is age, or (negative) distance from right edge
point_selected = QtCore.pyqtSignal(float, float)

def __init__(self, parent):
super().__init__(parent)
Expand Down Expand Up @@ -90,6 +95,9 @@ def __init__(self, parent):

AudioBackend().underflow.connect(self.PlotZoneImage.plotImage.canvasscaledspectrogram.syncOffsets)

self.PlotZoneImage.canvasWidget.point_selected.connect(
self.on_point_selected)

self.last_data_time = 0.

self.mustRestart = False
Expand Down Expand Up @@ -177,6 +185,12 @@ def restart(self):
# defer the restart until we get data from the audio source (so that a fresh lastdatatime is passed to the spectrogram image)
self.mustRestart = True

def on_point_selected(self, time: float, freq: float) -> None:
self.point_selected.emit(time - self.timerange_s, freq)

def connect_time_selected(self, slot: Callable[[float], None]) -> None:
self.point_selected.connect(lambda t, _f: slot(t))

def setminfreq(self, freq):
self.minfreq = freq
self.PlotZoneImage.setfreqrange(self.minfreq, self.maxfreq)
Expand Down
Loading