Compare commits
8 Commits
c292350b25
...
plugin_mig
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f367c9264 | |||
| 9af8626dab | |||
| 60355d176c | |||
| d6f4d4166b | |||
| f9fdfb629b | |||
| f3b883602e | |||
| 5516ce9212 | |||
| 0346efd504 |
1
audio-service/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
recordings/
|
||||||
1
audio-service/build.bat
Normal file
@ -0,0 +1 @@
|
|||||||
|
python -m venv venv
|
||||||
34
audio-service/metadata.json
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "Uncategorized",
|
||||||
|
"id": 0,
|
||||||
|
"clips": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Test",
|
||||||
|
"id": 1,
|
||||||
|
"clips": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "New",
|
||||||
|
"id": 2,
|
||||||
|
"clips": [
|
||||||
|
{
|
||||||
|
"endTime": 30,
|
||||||
|
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260220_193822.wav",
|
||||||
|
"name": "Pee pee\npoo poo",
|
||||||
|
"playbackType": "playStop",
|
||||||
|
"startTime": 27.756510985786615,
|
||||||
|
"volume": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"endTime": 28.597210828548004,
|
||||||
|
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260220_200442.wav",
|
||||||
|
"name": "Clip 20260220_200442",
|
||||||
|
"playbackType": "playStop",
|
||||||
|
"startTime": 26.1853978671042,
|
||||||
|
"volume": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@ -4,3 +4,4 @@ python-osc==1.9.3
|
|||||||
scipy==1.10.1
|
scipy==1.10.1
|
||||||
comtypes==1.4.8
|
comtypes==1.4.8
|
||||||
pycaw==20240210
|
pycaw==20240210
|
||||||
|
Flask==3.1.2
|
||||||
10
audio-service/settings.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"input_device": {
|
||||||
|
"default_samplerate": 44100.0,
|
||||||
|
"index": 1,
|
||||||
|
"max_input_channels": 8,
|
||||||
|
"name": "VM Mic mix (VB-Audio Voicemeete"
|
||||||
|
},
|
||||||
|
"save_path": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings",
|
||||||
|
"recording_length": 30
|
||||||
|
}
|
||||||
BIN
audio-service/src/__pycache__/audio_recorder.cpython-313.pyc
Normal file
BIN
audio-service/src/__pycache__/main.cpython-313.pyc
Normal file
BIN
audio-service/src/__pycache__/metadata_manager.cpython-313.pyc
Normal file
BIN
audio-service/src/__pycache__/osc_server.cpython-313.pyc
Normal file
BIN
audio-service/src/__pycache__/settings.cpython-313.pyc
Normal file
BIN
audio-service/src/__pycache__/windows_audio.cpython-313.pyc
Normal file
@ -3,9 +3,18 @@ import numpy as np
|
|||||||
import os
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import scipy.io.wavfile as wavfile
|
import scipy.io.wavfile as wavfile
|
||||||
|
from metadata_manager import MetaDataManager
|
||||||
|
|
||||||
class AudioRecorder:
|
class AudioRecorder:
|
||||||
def __init__(self, duration=30, sample_rate=44100, channels=2, recordings_dir='recordings'):
|
_instance = None
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
if cls._instance is None:
|
||||||
|
print("Creating new AudioRecorder instance")
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
cls._instance.init()
|
||||||
|
return cls._instance
|
||||||
|
def init(self):
|
||||||
"""
|
"""
|
||||||
Initialize audio recorder with configurable parameters.
|
Initialize audio recorder with configurable parameters.
|
||||||
|
|
||||||
@ -13,12 +22,39 @@ class AudioRecorder:
|
|||||||
:param sample_rate: Audio sample rate (if None, use default device sample rate)
|
:param sample_rate: Audio sample rate (if None, use default device sample rate)
|
||||||
:param channels: Number of audio channels
|
:param channels: Number of audio channels
|
||||||
"""
|
"""
|
||||||
|
print(f"Initializing AudioRecorder")
|
||||||
|
self.duration = 30
|
||||||
|
self.sample_rate = 44100
|
||||||
|
self.channels = 2
|
||||||
|
self.buffer = np.zeros((int(self.duration * self.sample_rate), self.channels), dtype=np.float32)
|
||||||
|
self.recordings_dir = "recordings"
|
||||||
|
self.stream = sd.InputStream(
|
||||||
|
samplerate=self.sample_rate,
|
||||||
|
channels=self.channels,
|
||||||
|
callback=self.record_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
def refresh_stream(self):
|
||||||
|
"""
|
||||||
|
Refresh the audio stream with updated parameters.
|
||||||
|
"""
|
||||||
|
was_active = self.stream.active
|
||||||
|
if was_active:
|
||||||
|
self.stream.stop()
|
||||||
|
|
||||||
|
self.buffer = np.zeros((int(self.duration * self.sample_rate), self.channels), dtype=np.float32)
|
||||||
|
|
||||||
|
self.stream = sd.InputStream(
|
||||||
|
samplerate=self.sample_rate,
|
||||||
|
channels=self.channels,
|
||||||
|
callback=self.record_callback
|
||||||
|
)
|
||||||
|
|
||||||
|
if was_active:
|
||||||
|
self.stream.start()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
self.duration = duration
|
|
||||||
self.sample_rate = sample_rate
|
|
||||||
self.channels = channels
|
|
||||||
self.buffer = np.zeros((int(duration * sample_rate), channels), dtype=np.float32)
|
|
||||||
self.recordings_dir = recordings_dir
|
|
||||||
|
|
||||||
def record_callback(self, indata, frames, time, status):
|
def record_callback(self, indata, frames, time, status):
|
||||||
"""
|
"""
|
||||||
@ -59,17 +95,62 @@ class AudioRecorder:
|
|||||||
# Write buffer to file
|
# Write buffer to file
|
||||||
wavfile.write(filename, int(self.sample_rate), audio_data_int16)
|
wavfile.write(filename, int(self.sample_rate), audio_data_int16)
|
||||||
|
|
||||||
return filename
|
meta = MetaDataManager()
|
||||||
|
|
||||||
|
clip_metadata = {
|
||||||
|
"filename": filename,
|
||||||
|
"name": f"Clip {timestamp}",
|
||||||
|
"playbackType":"playStop",
|
||||||
|
"volume": 1.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
meta.add_clip_to_collection("Uncategorized", clip_metadata )
|
||||||
|
|
||||||
|
|
||||||
|
return clip_metadata
|
||||||
|
|
||||||
|
def set_buffer_duration(self, duration):
|
||||||
|
"""
|
||||||
|
Set the duration of the audio buffer.
|
||||||
|
|
||||||
|
:param duration: New buffer duration in seconds
|
||||||
|
"""
|
||||||
|
self.duration = duration
|
||||||
|
self.buffer = np.zeros((int(duration * self.sample_rate), self.channels), dtype=np.float32)
|
||||||
|
|
||||||
|
def set_recording_directory(self, directory):
|
||||||
|
"""
|
||||||
|
Set the directory where recordings will be saved.
|
||||||
|
|
||||||
|
:param directory: Path to the recordings directory
|
||||||
|
"""
|
||||||
|
self.recordings_dir = directory
|
||||||
|
|
||||||
def start_recording(self):
|
def start_recording(self):
|
||||||
"""
|
"""
|
||||||
Start continuous audio recording with circular buffer.
|
Start continuous audio recording with circular buffer.
|
||||||
"""
|
"""
|
||||||
|
if(self.stream.active):
|
||||||
|
print("Already recording")
|
||||||
|
return
|
||||||
print('number of channels', self.channels)
|
print('number of channels', self.channels)
|
||||||
stream = sd.InputStream(
|
|
||||||
samplerate=self.sample_rate,
|
self.stream.start()
|
||||||
channels=self.channels,
|
|
||||||
callback=self.record_callback
|
def stop_recording(self):
|
||||||
)
|
"""
|
||||||
stream.start()
|
Stop continuous audio recording with circular buffer.
|
||||||
return stream
|
"""
|
||||||
|
if(not self.stream.active):
|
||||||
|
print("Already stopped")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.stream.stop()
|
||||||
|
|
||||||
|
def is_recording(self):
|
||||||
|
"""
|
||||||
|
Check if the audio stream is currently active.
|
||||||
|
|
||||||
|
:return: True if recording, False otherwise
|
||||||
|
"""
|
||||||
|
return self.stream.active
|
||||||
@ -1,12 +0,0 @@
|
|||||||
from pythonosc.udp_client import SimpleUDPClient
|
|
||||||
import sys
|
|
||||||
|
|
||||||
ip = "127.0.0.1"
|
|
||||||
port = 5005
|
|
||||||
|
|
||||||
client = SimpleUDPClient(ip, port) # Create client
|
|
||||||
# client.send_message("/record/start", 0)
|
|
||||||
#sleep(5)
|
|
||||||
client.send_message(sys.argv[1], sys.argv[2] if len(sys.argv) > 2 else 0)
|
|
||||||
#sleep(5)
|
|
||||||
# client.send_message("/record/stop", 0)
|
|
||||||
@ -1,92 +1,69 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from osc_server import OSCRecordingServer
|
|
||||||
from audio_recorder import AudioRecorder
|
from audio_recorder import AudioRecorder
|
||||||
from windows_audio import WindowsAudioManager
|
from windows_audio import WindowsAudioManager
|
||||||
import sounddevice as sd
|
import sounddevice as sd
|
||||||
|
from metadata_manager import MetaDataManager
|
||||||
|
from settings import SettingsManager
|
||||||
|
|
||||||
|
from flask import Flask
|
||||||
|
from flask_cors import CORS
|
||||||
|
|
||||||
|
from routes.recording import recording_bp
|
||||||
|
from routes.device import device_bp
|
||||||
|
from routes.metadata import metadata_bp
|
||||||
|
from routes.settings import settings_bp
|
||||||
|
from flask_socketio import SocketIO
|
||||||
|
import threading
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
CORS(app)
|
||||||
|
# socketio = SocketIO(app, cors_allowed_origins="*")
|
||||||
|
# CORS(socketio)
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# Create argument parser
|
# Create argument parser
|
||||||
parser = argparse.ArgumentParser(description='Audio Recording Service')
|
parser = argparse.ArgumentParser(description='Audio Recording Service')
|
||||||
|
|
||||||
# Input device argument
|
|
||||||
parser.add_argument('--input-device',
|
|
||||||
type=str,
|
|
||||||
help='Name or index of the input audio device',
|
|
||||||
default=None)
|
|
||||||
|
|
||||||
# Recording length argument
|
|
||||||
parser.add_argument('--recording-length',
|
|
||||||
type=float,
|
|
||||||
help='Maximum recording length in seconds',
|
|
||||||
default=30.0)
|
|
||||||
|
|
||||||
# Recording save path argument
|
|
||||||
parser.add_argument('--save-path',
|
|
||||||
type=str,
|
|
||||||
help='Directory path to save recordings',
|
|
||||||
default=os.path.join(os.path.dirname(__file__), 'recordings'))
|
|
||||||
|
|
||||||
# OSC port argument
|
# OSC port argument
|
||||||
parser.add_argument('--osc-port',
|
parser.add_argument('--osc-port',
|
||||||
type=int,
|
type=int,
|
||||||
help='OSC server port number',
|
help='OSC server port number',
|
||||||
default=5005)
|
default=5010)
|
||||||
|
|
||||||
# Parse arguments
|
# Parse arguments
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
audio_manager = WindowsAudioManager()
|
||||||
|
settings = SettingsManager()
|
||||||
|
|
||||||
# Ensure save path exists
|
# Ensure save path exists
|
||||||
os.makedirs(args.save_path, exist_ok=True)
|
os.makedirs(settings.get_settings('save_path'), exist_ok=True)
|
||||||
audio_manager=WindowsAudioManager()
|
|
||||||
# Handle input device selection
|
|
||||||
input_device = None
|
|
||||||
devices = audio_manager.list_audio_devices('input')
|
|
||||||
if args.input_device:
|
|
||||||
try:
|
|
||||||
# Try to convert to integer first (for device index)
|
|
||||||
input_device = int(args.input_device)
|
|
||||||
except ValueError:
|
|
||||||
# If not an integer, treat as device name
|
|
||||||
|
|
||||||
print(devices)
|
|
||||||
for i, device in enumerate(devices):
|
|
||||||
if args.input_device.lower() in device['name'].lower():
|
|
||||||
input_device = device['index']
|
|
||||||
print(f"Using input device: {device['name']}")
|
|
||||||
break
|
|
||||||
|
|
||||||
# Create AudioRecorder with specified parameters
|
# Register blueprints
|
||||||
recorder = AudioRecorder(
|
app.register_blueprint(recording_bp)
|
||||||
duration=args.recording_length,
|
app.register_blueprint(device_bp)
|
||||||
recordings_dir=args.save_path,
|
app.register_blueprint(metadata_bp)
|
||||||
# channels=min(2, devices[input_device]['max_input_channels']),
|
app.register_blueprint(settings_bp)
|
||||||
)
|
app.run(host='127.0.0.1', port=args.osc_port, debug=False, use_reloader=True)
|
||||||
|
# socketio.run(app, host='127.0.0.1', port=args.osc_port, debug=False, use_reloader=True)
|
||||||
|
|
||||||
# Create OSC server with specified port
|
|
||||||
osc_server = OSCRecordingServer(
|
|
||||||
recorder=recorder,
|
|
||||||
port=args.osc_port,
|
|
||||||
audio_manager=audio_manager
|
|
||||||
)
|
|
||||||
|
|
||||||
osc_server.set_audio_device(None, str(input_device))
|
|
||||||
osc_server.start_recording(None)
|
|
||||||
|
|
||||||
# Run the OSC server
|
# Run the OSC server
|
||||||
try:
|
try:
|
||||||
print(f"Starting OSC Recording Server on port {args.osc_port}")
|
print(f"Starting OSC Recording Server on port {args.osc_port}")
|
||||||
print(f"Recording save path: {args.save_path}")
|
|
||||||
print(f"Max recording length: {args.recording_length} seconds")
|
|
||||||
|
|
||||||
|
|
||||||
osc_server.run_server()
|
|
||||||
|
# osc_server.run_server()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\nServer stopped by user.")
|
print("\nServer stopped by user.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error starting server: {e}")
|
print(f"Error starting server: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
20
audio-service/src/metadata.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"Uncategorized": [
|
||||||
|
{
|
||||||
|
"endTime": 12.489270386266055,
|
||||||
|
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\src\\recordings\\audio_capture_20260214_133540.wav",
|
||||||
|
"name": "Clip 20260214_133540",
|
||||||
|
"playbackType": "playStop",
|
||||||
|
"startTime": 10.622317596566523,
|
||||||
|
"volume": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"endTime": 6.824034334763957,
|
||||||
|
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\src\\recordings\\audio_capture_20260214_133137.wav",
|
||||||
|
"name": "Clip 20260214_133137",
|
||||||
|
"playbackType": "playStop",
|
||||||
|
"startTime": 3.7982832618025753,
|
||||||
|
"volume": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
100
audio-service/src/metadata_manager.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
class MetaDataManager:
|
||||||
|
_instance = None
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
cls._instance.init()
|
||||||
|
return cls._instance
|
||||||
|
def init(self):
|
||||||
|
# read metadata file from executing directory
|
||||||
|
self.metadata_file = os.path.join(os.getcwd(), "metadata.json")
|
||||||
|
if os.path.exists(self.metadata_file):
|
||||||
|
with open(self.metadata_file, "r") as f:
|
||||||
|
self.collections = json.load(f)
|
||||||
|
else:
|
||||||
|
self.collections = {}
|
||||||
|
if(collections := next((c for c in self.collections if c.get("name") == "Uncategorized"), None)) is None:
|
||||||
|
self.collections.append({"name": "Uncategorized", "id": 0, "clips": []})
|
||||||
|
self.save_metadata()
|
||||||
|
|
||||||
|
def create_collection(self, name):
|
||||||
|
if any(c.get("name") == name for c in self.collections):
|
||||||
|
raise ValueError(f"Collection '{name}' already exists.")
|
||||||
|
new_id = max((c.get("id", 0) for c in self.collections), default=0) + 1
|
||||||
|
self.collections.append({"name": name, "id": new_id, "clips": []})
|
||||||
|
self.save_metadata()
|
||||||
|
|
||||||
|
def delete_collection(self, name):
|
||||||
|
collection = next((c for c in self.collections if c.get("name") == name), None)
|
||||||
|
if collection is None:
|
||||||
|
raise ValueError(f"Collection '{name}' does not exist.")
|
||||||
|
self.collections.remove(collection)
|
||||||
|
self.save_metadata()
|
||||||
|
|
||||||
|
def add_clip_to_collection(self, collection_name, clip_metadata):
|
||||||
|
collection = next((c for c in self.collections if c.get("name") == collection_name), None)
|
||||||
|
if collection is None:
|
||||||
|
raise ValueError(f"Collection '{collection_name}' does not exist.")
|
||||||
|
collection["clips"].append(clip_metadata)
|
||||||
|
self.save_metadata()
|
||||||
|
|
||||||
|
def remove_clip_from_collection(self, collection_name, clip_metadata):
|
||||||
|
collection = next((c for c in self.collections if c.get("name") == collection_name), None)
|
||||||
|
if collection is None:
|
||||||
|
raise ValueError(f"Collection '{collection_name}' does not exist.")
|
||||||
|
# Remove all clips with the same file name as clip_metadata["file_name"]
|
||||||
|
in_list = any(clip.get("filename") == clip_metadata.get("filename") for clip in collection["clips"])
|
||||||
|
if not in_list:
|
||||||
|
raise ValueError(f"Clip with filename '{clip_metadata.get('filename')}' not found in collection '{collection_name}'.")
|
||||||
|
|
||||||
|
collection["clips"] = [
|
||||||
|
clip for clip in collection["clips"]
|
||||||
|
if clip.get("filename") != clip_metadata.get("filename")
|
||||||
|
]
|
||||||
|
self.save_metadata()
|
||||||
|
|
||||||
|
def move_clip_to_collection(self, source_collection, target_collection, clip_metadata):
|
||||||
|
self.remove_clip_from_collection(source_collection, clip_metadata)
|
||||||
|
self.add_clip_to_collection(target_collection, clip_metadata)
|
||||||
|
|
||||||
|
def edit_clip_in_collection(self, collection_name, new_clip_metadata):
|
||||||
|
collection = next((c for c in self.collections if c.get("name") == collection_name), None)
|
||||||
|
if collection is None:
|
||||||
|
raise ValueError(f"Collection '{collection_name}' does not exist.")
|
||||||
|
# Find the index of the clip with the same file name as old_clip_metadata["file_name"]
|
||||||
|
index = next((i for i, clip in enumerate(collection["clips"]) if clip.get("filename") == new_clip_metadata.get("filename")), None)
|
||||||
|
if index is None:
|
||||||
|
raise ValueError(f"Clip with filename '{new_clip_metadata.get('filename')}' not found in collection '{collection_name}'.")
|
||||||
|
|
||||||
|
collection["clips"][index] = new_clip_metadata
|
||||||
|
self.save_metadata()
|
||||||
|
|
||||||
|
def get_collections(self):
|
||||||
|
return list(map(lambda c: {"name": c.get("name"), "id": c.get("id")}, self.collections))
|
||||||
|
|
||||||
|
def get_clips_in_collection(self, collection_name):
|
||||||
|
collection = next((c for c in self.collections if c.get("name") == collection_name), None)
|
||||||
|
if collection is None:
|
||||||
|
raise ValueError(f"Collection '{collection_name}' does not exist.")
|
||||||
|
return collection["clips"]
|
||||||
|
|
||||||
|
def reorder_clips_in_collection(self, collection_name, new_order):
|
||||||
|
collection = next((c for c in self.collections if c.get("name") == collection_name), None)
|
||||||
|
if collection is None:
|
||||||
|
raise ValueError(f"Collection '{collection_name}' does not exist.")
|
||||||
|
existing_filenames = {clip.get("filename") for clip in collection["clips"]}
|
||||||
|
new_filenames = {clip.get("filename") for clip in new_order}
|
||||||
|
|
||||||
|
if not new_filenames.issubset(existing_filenames):
|
||||||
|
raise ValueError("New order contains clips that do not exist in the collection.")
|
||||||
|
|
||||||
|
collection["clips"] = new_order
|
||||||
|
self.save_metadata()
|
||||||
|
|
||||||
|
def save_metadata(self):
|
||||||
|
with open(self.metadata_file, "w") as f:
|
||||||
|
json.dump(self.collections, f, indent=4)
|
||||||
@ -1,130 +0,0 @@
|
|||||||
from pythonosc import dispatcher, osc_server
|
|
||||||
import threading
|
|
||||||
import sys
|
|
||||||
from audio_recorder import AudioRecorder
|
|
||||||
from windows_audio import WindowsAudioManager
|
|
||||||
|
|
||||||
class OSCRecordingServer:
|
|
||||||
def __init__(self, recorder, audio_manager, ip="127.0.0.1", port=5005):
|
|
||||||
"""
|
|
||||||
Initialize OSC server for audio recording triggers.
|
|
||||||
|
|
||||||
:param recorder: AudioRecorder instance
|
|
||||||
:param audio_manager: WindowsAudioManager instance
|
|
||||||
:param ip: IP address to bind OSC server
|
|
||||||
:param port: Port number for OSC server
|
|
||||||
"""
|
|
||||||
self.recorder = recorder
|
|
||||||
self.audio_manager = audio_manager
|
|
||||||
self.ip = ip
|
|
||||||
self.port = port
|
|
||||||
self._setup_dispatcher()
|
|
||||||
self.server_thread = None
|
|
||||||
|
|
||||||
def _setup_dispatcher(self):
|
|
||||||
"""
|
|
||||||
Set up OSC message dispatchers for different recording commands.
|
|
||||||
"""
|
|
||||||
self.osc_dispatcher = dispatcher.Dispatcher()
|
|
||||||
self.osc_dispatcher.map("/record/start", self.start_recording)
|
|
||||||
self.osc_dispatcher.map("/record/stop", self.stop_recording)
|
|
||||||
self.osc_dispatcher.map("/record/save", self.save_recording)
|
|
||||||
self.osc_dispatcher.map("/exit", self.exit_program)
|
|
||||||
self.osc_dispatcher.map("/device/set", self.set_audio_device) # New device set handler
|
|
||||||
self.osc_dispatcher.map("/device/list", self.list_audio_devices) # New device list handler
|
|
||||||
|
|
||||||
def start_recording(self, unused_addr, args=None):
|
|
||||||
"""
|
|
||||||
Start audio recording via OSC message.
|
|
||||||
"""
|
|
||||||
print("OSC: Starting audio recording")
|
|
||||||
self.recording_stream = self.recorder.start_recording()
|
|
||||||
|
|
||||||
def stop_recording(self, unused_addr, args=None):
|
|
||||||
"""
|
|
||||||
Stop active audio recording via OSC message.
|
|
||||||
"""
|
|
||||||
print("OSC: Stopping audio recording")
|
|
||||||
if hasattr(self, 'recording_stream'):
|
|
||||||
self.recording_stream.stop()
|
|
||||||
|
|
||||||
def save_recording(self, unused_addr, args=None):
|
|
||||||
"""
|
|
||||||
Save the current audio buffer via OSC message.
|
|
||||||
"""
|
|
||||||
print("OSC: Saving audio recording")
|
|
||||||
saved_file = self.recorder.save_last_n_seconds()
|
|
||||||
print(f"Saved recording to: {saved_file}")
|
|
||||||
|
|
||||||
def set_audio_device(self, unused_addr, device_index):
|
|
||||||
"""
|
|
||||||
Set the default input audio device via OSC message.
|
|
||||||
|
|
||||||
:param device_index: Index of the audio device to set
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
device_index = int(device_index)
|
|
||||||
print(f"OSC: Setting audio device to index {device_index}")
|
|
||||||
|
|
||||||
# Get the sample rate of the new device
|
|
||||||
sample_rate = self.audio_manager.set_default_input_device(device_index)
|
|
||||||
|
|
||||||
# Reinitialize recorder with new device's sample rate
|
|
||||||
self.recorder = AudioRecorder(
|
|
||||||
duration=self.recorder.duration,
|
|
||||||
sample_rate=sample_rate,
|
|
||||||
channels=self.recorder.channels,
|
|
||||||
recordings_dir=self.recorder.recordings_dir
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f"Successfully set audio device to index {device_index} with sample rate {sample_rate}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"OSC: Error setting audio device - {e}")
|
|
||||||
|
|
||||||
def list_audio_devices(self, unused_addr, device_type='input'):
|
|
||||||
"""
|
|
||||||
List available audio devices via OSC message.
|
|
||||||
|
|
||||||
:param device_type: 'input' or 'output'
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
devices = self.audio_manager.list_audio_devices(device_type)
|
|
||||||
print(f"Available {device_type} devices:")
|
|
||||||
for idx, device in enumerate(devices):
|
|
||||||
print(f"Index {device['index']}: {device['name']}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"OSC: Error listing audio devices - {e}")
|
|
||||||
|
|
||||||
def exit_program(self, unused_addr, args=None):
|
|
||||||
"""
|
|
||||||
Gracefully exit the program via OSC message.
|
|
||||||
"""
|
|
||||||
print("OSC: Received exit command. Shutting down...")
|
|
||||||
if hasattr(self, 'recording_stream'):
|
|
||||||
self.recording_stream.stop()
|
|
||||||
|
|
||||||
if self.server_thread:
|
|
||||||
self.server.shutdown()
|
|
||||||
self.server_thread.join()
|
|
||||||
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
def run_server(self):
|
|
||||||
"""
|
|
||||||
Start the OSC server in a separate thread.
|
|
||||||
"""
|
|
||||||
self.server = osc_server.ThreadingOSCUDPServer(
|
|
||||||
(self.ip, self.port),
|
|
||||||
self.osc_dispatcher
|
|
||||||
)
|
|
||||||
|
|
||||||
self.server_thread = threading.Thread(target=self.server.serve_forever)
|
|
||||||
self.server_thread.start()
|
|
||||||
return self.server_thread
|
|
||||||
|
|
||||||
def stop_server(self):
|
|
||||||
"""
|
|
||||||
Stop the OSC server.
|
|
||||||
"""
|
|
||||||
if hasattr(self, 'server'):
|
|
||||||
self.server.shutdown()
|
|
||||||
BIN
audio-service/src/recordings/audio_capture_20260214_092540.wav
Normal file
BIN
audio-service/src/routes/__pycache__/device.cpython-313.pyc
Normal file
BIN
audio-service/src/routes/__pycache__/metadata.cpython-313.pyc
Normal file
BIN
audio-service/src/routes/__pycache__/recording.cpython-313.pyc
Normal file
BIN
audio-service/src/routes/__pycache__/settings.cpython-313.pyc
Normal file
37
audio-service/src/routes/device.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
from windows_audio import WindowsAudioManager
|
||||||
|
from audio_recorder import AudioRecorder
|
||||||
|
|
||||||
|
device_bp = Blueprint('device', __name__)
|
||||||
|
|
||||||
|
audio_manager = WindowsAudioManager()
|
||||||
|
recorder = AudioRecorder()
|
||||||
|
|
||||||
|
# @device_bp.route('/device/set', methods=['POST'])
|
||||||
|
# def set_audio_device():
|
||||||
|
# device_index = request.json.get('device_index')
|
||||||
|
# try:
|
||||||
|
# device_index = int(device_index)
|
||||||
|
# print(f'HTTP: Setting audio device to index {device_index}')
|
||||||
|
# sample_rate = audio_manager.set_default_input_device(device_index)
|
||||||
|
# recorder.sample_rate = sample_rate
|
||||||
|
# return jsonify({'status': 'device set', 'device_index': device_index, 'sample_rate': sample_rate})
|
||||||
|
# except Exception as e:
|
||||||
|
# return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
|
||||||
|
@device_bp.route('/device/get', methods=['GET'])
|
||||||
|
def get_audio_device():
|
||||||
|
try:
|
||||||
|
device_info = audio_manager.get_default_device('input')
|
||||||
|
return jsonify({'status': 'success', 'device_info': device_info})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
|
||||||
|
@device_bp.route('/device/list', methods=['GET'])
|
||||||
|
def list_audio_devices():
|
||||||
|
device_type = request.args.get('device_type', 'input')
|
||||||
|
try:
|
||||||
|
devices = audio_manager.list_audio_devices(device_type)
|
||||||
|
return jsonify({'status': 'success', 'devices': devices})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
100
audio-service/src/routes/metadata.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
|
||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
from metadata_manager import MetaDataManager
|
||||||
|
|
||||||
|
metadata_bp = Blueprint('metadata', __name__)
|
||||||
|
|
||||||
|
@metadata_bp.route('/meta', methods=['GET'])
|
||||||
|
def get_allmetadata():
|
||||||
|
meta_manager = MetaDataManager()
|
||||||
|
collections = meta_manager.collections
|
||||||
|
return jsonify({'status': 'success', 'collections': collections})
|
||||||
|
|
||||||
|
@metadata_bp.route('/meta/collections', methods=['GET'])
|
||||||
|
def get_collections():
|
||||||
|
meta_manager = MetaDataManager()
|
||||||
|
collections = meta_manager.get_collections()
|
||||||
|
return jsonify({'status': 'success', 'collections': collections})
|
||||||
|
|
||||||
|
@metadata_bp.route('/meta/collections/add', methods=['POST'])
|
||||||
|
def add_collection():
|
||||||
|
meta_manager = MetaDataManager()
|
||||||
|
collection_name = request.json.get('name')
|
||||||
|
try:
|
||||||
|
meta_manager.create_collection(collection_name)
|
||||||
|
collections = meta_manager.collections
|
||||||
|
return jsonify({'status': 'success', 'collections': collections})
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
|
||||||
|
@metadata_bp.route('/meta/collection/clips/<name>', methods=['GET'])
|
||||||
|
def get_clips_in_collection(name):
|
||||||
|
meta_manager = MetaDataManager()
|
||||||
|
collection_name = name
|
||||||
|
try:
|
||||||
|
clips = meta_manager.get_clips_in_collection(collection_name)
|
||||||
|
return jsonify({'status': 'success', 'clips': clips})
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
|
||||||
|
@metadata_bp.route('/meta/collection/clips/reorder', methods=['POST'])
|
||||||
|
def reorder_clips_in_collection():
|
||||||
|
meta_manager = MetaDataManager()
|
||||||
|
collection_name = request.json.get('name')
|
||||||
|
new_order = request.json.get('clips')
|
||||||
|
try:
|
||||||
|
meta_manager.reorder_clips_in_collection(collection_name, new_order)
|
||||||
|
collections = meta_manager.collections
|
||||||
|
return jsonify({'status': 'success', 'collections': collections})
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
|
||||||
|
@metadata_bp.route('/meta/collection/clips/add', methods=['POST'])
|
||||||
|
def add_clip_to_collection():
|
||||||
|
meta_manager = MetaDataManager()
|
||||||
|
collection_name = request.json.get('name')
|
||||||
|
clip_metadata = request.json.get('clip')
|
||||||
|
try:
|
||||||
|
meta_manager.add_clip_to_collection(collection_name, clip_metadata)
|
||||||
|
collections = meta_manager.collections
|
||||||
|
return jsonify({'status': 'success', 'collections': collections})
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
|
||||||
|
@metadata_bp.route('/meta/collection/clips/remove', methods=['POST'])
|
||||||
|
def remove_clip_from_collection():
|
||||||
|
meta_manager = MetaDataManager()
|
||||||
|
collection_name = request.json.get('name')
|
||||||
|
clip_metadata = request.json.get('clip')
|
||||||
|
try:
|
||||||
|
meta_manager.remove_clip_from_collection(collection_name, clip_metadata)
|
||||||
|
clips = meta_manager.get_clips_in_collection(collection_name)
|
||||||
|
return jsonify({'status': 'success', 'clips': clips})
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
|
||||||
|
@metadata_bp.route('/meta/collection/clips/move', methods=['POST'])
|
||||||
|
def move_clip_to_collection():
|
||||||
|
meta_manager = MetaDataManager()
|
||||||
|
sourceCollection = request.json.get('sourceCollection')
|
||||||
|
targetCollection = request.json.get('targetCollection')
|
||||||
|
clip_metadata = request.json.get('clip')
|
||||||
|
try:
|
||||||
|
meta_manager.move_clip_to_collection(sourceCollection, targetCollection, clip_metadata)
|
||||||
|
collections = meta_manager.collections
|
||||||
|
return jsonify({'status': 'success', 'collections': collections})
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
|
||||||
|
@metadata_bp.route('/meta/collection/clips/edit', methods=['POST'])
|
||||||
|
def edit_clip_in_collection():
|
||||||
|
meta_manager = MetaDataManager()
|
||||||
|
collection_name = request.json.get('name')
|
||||||
|
clip_metadata = request.json.get('clip')
|
||||||
|
print(f"Received request to edit clip in collection '{collection_name}': {clip_metadata}")
|
||||||
|
try:
|
||||||
|
meta_manager.edit_clip_in_collection(collection_name, clip_metadata)
|
||||||
|
collections = meta_manager.collections
|
||||||
|
return jsonify({'status': 'success', 'collections': collections})
|
||||||
|
except ValueError as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
53
audio-service/src/routes/recording.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
|
||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
from audio_recorder import AudioRecorder
|
||||||
|
import os
|
||||||
|
|
||||||
|
recording_bp = Blueprint('recording', __name__)
|
||||||
|
|
||||||
|
@recording_bp.route('/record/start', methods=['POST'])
|
||||||
|
def start_recording():
|
||||||
|
recorder = AudioRecorder()
|
||||||
|
print('HTTP: Starting audio recording')
|
||||||
|
recorder.start_recording()
|
||||||
|
return jsonify({'status': 'recording started'})
|
||||||
|
|
||||||
|
@recording_bp.route('/record/stop', methods=['POST'])
|
||||||
|
def stop_recording():
|
||||||
|
recorder = AudioRecorder()
|
||||||
|
print('HTTP: Stopping audio recording')
|
||||||
|
recorder.stop_recording()
|
||||||
|
return jsonify({'status': 'recording stopped'})
|
||||||
|
|
||||||
|
@recording_bp.route('/record/save', methods=['POST'])
|
||||||
|
def save_recording():
|
||||||
|
recorder = AudioRecorder()
|
||||||
|
print('HTTP: Saving audio recording')
|
||||||
|
saved_file = recorder.save_last_n_seconds()
|
||||||
|
return jsonify({'status': 'recording saved', 'file': saved_file})
|
||||||
|
|
||||||
|
|
||||||
|
@recording_bp.route('/record/status', methods=['GET'])
|
||||||
|
def recording_status():
|
||||||
|
recorder = AudioRecorder()
|
||||||
|
print('HTTP: Checking recording status')
|
||||||
|
status = 'recording' if recorder.is_recording() else 'stopped'
|
||||||
|
return jsonify({'status': status})
|
||||||
|
|
||||||
|
@recording_bp.route('/record/delete', methods=['POST'])
|
||||||
|
def recording_delete():
|
||||||
|
filename = request.json.get('filename')
|
||||||
|
try:
|
||||||
|
os.remove(filename)
|
||||||
|
return jsonify({'status': 'success'})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
|
|
||||||
|
@recording_bp.route('/playback/start', methods=['POST'])
|
||||||
|
def playback_start():
|
||||||
|
print('HTTP: Starting audio playback')
|
||||||
|
try:
|
||||||
|
# os.remove(filename)
|
||||||
|
return jsonify({'status': 'success'})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||||||
25
audio-service/src/routes/settings.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
from flask import Blueprint, request, jsonify
|
||||||
|
from settings import SettingsManager
|
||||||
|
|
||||||
|
settings_bp = Blueprint('settings', __name__)
|
||||||
|
|
||||||
|
|
||||||
|
@settings_bp.route('/settings', methods=['GET'])
|
||||||
|
def get_all_settings():
|
||||||
|
return jsonify({'status': 'success', 'settings': SettingsManager().get_all_settings()})
|
||||||
|
|
||||||
|
@settings_bp.route('/settings/<name>', methods=['GET'])
|
||||||
|
def get_setting(name):
|
||||||
|
value = SettingsManager().get_settings(name)
|
||||||
|
if value is not None:
|
||||||
|
return jsonify({'status': 'success', 'name': name, 'value': value})
|
||||||
|
else:
|
||||||
|
return jsonify({'status': 'error', 'message': f'Setting "{name}" not found'}), 404
|
||||||
|
|
||||||
|
@settings_bp.route('/settings/<name>', methods=['POST'])
|
||||||
|
def set_setting(name):
|
||||||
|
value = request.json.get('value')
|
||||||
|
if value is None:
|
||||||
|
return jsonify({'status': 'error', 'message': 'Value is required'}), 400
|
||||||
|
SettingsManager().set_settings(name, value)
|
||||||
|
return jsonify({'status': 'success', 'name': name, 'value': value})
|
||||||
10
audio-service/src/settings.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"input_device": {
|
||||||
|
"index": 0,
|
||||||
|
"name": "Microsoft Sound Mapper - Input",
|
||||||
|
"max_input_channels": 2,
|
||||||
|
"default_samplerate": 44100.0
|
||||||
|
},
|
||||||
|
"save_path": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\src\\recordings",
|
||||||
|
"recording_length": 15
|
||||||
|
}
|
||||||
69
audio-service/src/settings.py
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
from audio_recorder import AudioRecorder
|
||||||
|
from windows_audio import WindowsAudioManager
|
||||||
|
|
||||||
|
class SettingsManager:
|
||||||
|
_instance = None
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
cls._instance.init()
|
||||||
|
return cls._instance
|
||||||
|
def init(self):
|
||||||
|
# read settings file from executing directory
|
||||||
|
self.settings_file = os.path.join(os.getcwd(), "settings.json")
|
||||||
|
if os.path.exists(self.settings_file):
|
||||||
|
with open(self.settings_file, "r") as f:
|
||||||
|
self.settings = json.load(f)
|
||||||
|
else:
|
||||||
|
self.settings = {
|
||||||
|
"input_device": None,
|
||||||
|
"save_path": os.path.join(os.getcwd(), "recordings"),
|
||||||
|
"recording_length": 15
|
||||||
|
}
|
||||||
|
audio_manager = WindowsAudioManager()
|
||||||
|
|
||||||
|
devices = audio_manager.list_audio_devices('input')
|
||||||
|
print(f"Available input devices: {self.settings}")
|
||||||
|
input = self.settings["input_device"]
|
||||||
|
|
||||||
|
#see if input device is in "devices", if not set to the first index
|
||||||
|
if input is not None and any(d['name'] == input["name"] for d in devices):
|
||||||
|
print(f"Using saved input device index: {input}")
|
||||||
|
else:
|
||||||
|
input = devices[0] if devices else None
|
||||||
|
self.settings["input_device"] = input
|
||||||
|
self.save_settings()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def get_settings(self, name):
|
||||||
|
# print(f"Getting setting '{name}': {self.settings}")
|
||||||
|
return self.settings.get(name, None)
|
||||||
|
|
||||||
|
def get_all_settings(self):
|
||||||
|
return self.settings
|
||||||
|
|
||||||
|
def set_settings(self, name, value):
|
||||||
|
self.settings[name] = value
|
||||||
|
self.save_settings()
|
||||||
|
|
||||||
|
def save_settings(self):
|
||||||
|
self.refresh_settings()
|
||||||
|
with open(self.settings_file, "w") as f:
|
||||||
|
json.dump(self.settings, f, indent=4)
|
||||||
|
|
||||||
|
def refresh_settings(self):
|
||||||
|
recorder = AudioRecorder()
|
||||||
|
# Update recorder parameters based on new setting
|
||||||
|
recorder.set_buffer_duration(self.get_settings('recording_length'))
|
||||||
|
recorder.recordings_dir = self.get_settings('save_path')
|
||||||
|
|
||||||
|
audio_manager = WindowsAudioManager()
|
||||||
|
audio_manager.set_default_input_device(self.get_settings('input_device')['index'])
|
||||||
|
|
||||||
|
recorder.refresh_stream()
|
||||||
|
|
||||||
@ -7,7 +7,14 @@ from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
class WindowsAudioManager:
|
class WindowsAudioManager:
|
||||||
def __init__(self):
|
_instance = None
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
cls._instance.init()
|
||||||
|
return cls._instance
|
||||||
|
def init(self):
|
||||||
"""
|
"""
|
||||||
Initialize Windows audio device and volume management.
|
Initialize Windows audio device and volume management.
|
||||||
"""
|
"""
|
||||||
@ -42,8 +49,27 @@ class WindowsAudioManager:
|
|||||||
}
|
}
|
||||||
for dev in self.devices if dev['max_output_channels'] > 0
|
for dev in self.devices if dev['max_output_channels'] > 0
|
||||||
]
|
]
|
||||||
|
def get_default_device(self, kind='input'):
|
||||||
|
"""
|
||||||
|
Get the default audio device.
|
||||||
|
|
||||||
|
:param kind: 'input' or 'output'
|
||||||
|
:return: Default audio device information
|
||||||
|
"""
|
||||||
|
if kind == 'input':
|
||||||
|
dev = self.devices[self.default_input]
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'index': dev['index'],
|
||||||
|
'name': dev['name'],
|
||||||
|
'max_input_channels': dev['max_input_channels'],
|
||||||
|
'default_samplerate': dev['default_samplerate']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
def set_default_input_device(self, device_index):
|
def set_default_input_device(self, device_index):
|
||||||
|
if(device_index is None):
|
||||||
|
return self.get_current_input_device_sample_rate()
|
||||||
"""
|
"""
|
||||||
Set the default input audio device.
|
Set the default input audio device.
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,9 @@ module.exports = {
|
|||||||
'no-unused-vars': 'off',
|
'no-unused-vars': 'off',
|
||||||
'@typescript-eslint/no-unused-vars': 'error',
|
'@typescript-eslint/no-unused-vars': 'error',
|
||||||
'react/require-default-props': 'off',
|
'react/require-default-props': 'off',
|
||||||
|
'react/jsx-no-bind': 'off',
|
||||||
|
'jsx-a11y/no-autofocus': 'off',
|
||||||
|
'no-console': 'off',
|
||||||
},
|
},
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 2022,
|
ecmaVersion: 2022,
|
||||||
|
|||||||
@ -1,121 +0,0 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
|
||||||
import WaveSurfer from 'wavesurfer.js';
|
|
||||||
import RegionsPlugin from 'wavesurfer.js/dist/plugin/wavesurfer.regions.js';
|
|
||||||
import { ipcRenderer } from 'electron';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
interface AudioTrimmerProps {
|
|
||||||
filePath: string;
|
|
||||||
section: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AudioTrimmer: React.FC<AudioTrimmerProps> = ({ filePath, section }) => {
|
|
||||||
const [trimStart, setTrimStart] = useState<number>(0);
|
|
||||||
const [trimEnd, setTrimEnd] = useState<number>(0);
|
|
||||||
const [isPlaying, setIsPlaying] = useState<boolean>(false);
|
|
||||||
const waveformRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
const wavesurferRef = useRef<any>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const loadTrimInfo = async () => {
|
|
||||||
const savedTrimInfo = await ipcRenderer.invoke('get-trim-info', section, path.basename(filePath));
|
|
||||||
setTrimStart(savedTrimInfo.trimStart || 0);
|
|
||||||
setTrimEnd(savedTrimInfo.trimEnd || 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
loadTrimInfo();
|
|
||||||
|
|
||||||
wavesurferRef.current = WaveSurfer.create({
|
|
||||||
container: waveformRef.current!,
|
|
||||||
waveColor: '#ccb1ff',
|
|
||||||
progressColor: '#6e44ba',
|
|
||||||
responsive: true,
|
|
||||||
height: 100,
|
|
||||||
hideScrollbar: true,
|
|
||||||
plugins: [
|
|
||||||
RegionsPlugin.create({
|
|
||||||
color: 'rgba(132, 81, 224, 0.3)',
|
|
||||||
drag: false,
|
|
||||||
resize: true,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
wavesurferRef.current.load(`file://${filePath}`);
|
|
||||||
|
|
||||||
wavesurferRef.current.on('ready', () => {
|
|
||||||
wavesurferRef.current.addRegion({
|
|
||||||
start: trimStart,
|
|
||||||
end: trimEnd,
|
|
||||||
color: 'rgba(132, 81, 224, 0.3)',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
wavesurferRef.current.on('region-update-end', (region: any) => {
|
|
||||||
setTrimStart(region.start);
|
|
||||||
setTrimEnd(region.end);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
wavesurferRef.current.destroy();
|
|
||||||
};
|
|
||||||
}, [filePath, section, trimStart, trimEnd]);
|
|
||||||
|
|
||||||
const handlePlayPause = () => {
|
|
||||||
if (isPlaying) {
|
|
||||||
wavesurferRef.current.pause();
|
|
||||||
} else {
|
|
||||||
wavesurferRef.current.play(trimStart, trimEnd);
|
|
||||||
}
|
|
||||||
setIsPlaying(!isPlaying);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveTrim = async () => {
|
|
||||||
const newTitle = prompt('Enter a title for the trimmed audio:');
|
|
||||||
if (newTitle) {
|
|
||||||
await ipcRenderer.invoke('save-trimmed-file', {
|
|
||||||
originalFilePath: filePath,
|
|
||||||
trimStart,
|
|
||||||
trimEnd,
|
|
||||||
title: newTitle,
|
|
||||||
});
|
|
||||||
alert('Trimmed audio saved successfully!');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
|
||||||
const confirmDelete = confirm('Are you sure you want to delete this audio file?');
|
|
||||||
if (confirmDelete) {
|
|
||||||
await ipcRenderer.invoke('delete-file', filePath);
|
|
||||||
alert('File deleted successfully!');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="audio-trimmer-item">
|
|
||||||
<div className="audio-trimmer-header">
|
|
||||||
<div className="audio-trimmer-title">{path.basename(filePath)}</div>
|
|
||||||
<div className="audio-trimmer-controls">
|
|
||||||
<button onClick={handlePlayPause}>
|
|
||||||
{isPlaying ? 'Pause' : 'Play'}
|
|
||||||
</button>
|
|
||||||
<button onClick={handleSaveTrim}>Save Trim</button>
|
|
||||||
<button onClick={handleDelete}>Delete</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div ref={waveformRef} className="waveform"></div>
|
|
||||||
<div className="trim-info">
|
|
||||||
<div>Start: {formatTime(trimStart)}</div>
|
|
||||||
<div>End: {formatTime(trimEnd)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatTime = (seconds: number) => {
|
|
||||||
const minutes = Math.floor(seconds / 60);
|
|
||||||
const remainingSeconds = Math.floor(seconds % 60);
|
|
||||||
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AudioTrimmer;
|
|
||||||
@ -1,138 +0,0 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
|
||||||
import WaveSurfer from 'wavesurfer.js';
|
|
||||||
import RegionsPlugin from 'wavesurfer.js/dist/plugin/wavesurfer.regions.js';
|
|
||||||
import { ipcRenderer } from 'electron';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
interface AudioTrimmerProps {
|
|
||||||
filePath: string;
|
|
||||||
section: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AudioTrimmer: React.FC<AudioTrimmerProps> = ({ filePath, section }) => {
|
|
||||||
const [trimStart, setTrimStart] = useState(0);
|
|
||||||
const [trimEnd, setTrimEnd] = useState(0);
|
|
||||||
const [title, setTitle] = useState('');
|
|
||||||
const waveformRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
const wavesurferRef = useRef<WaveSurfer | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const loadTrimInfo = async () => {
|
|
||||||
const savedTrimInfo = await ipcRenderer.invoke('get-trim-info', section, path.basename(filePath));
|
|
||||||
setTrimStart(savedTrimInfo.trimStart || 0);
|
|
||||||
setTrimEnd(savedTrimInfo.trimEnd || 0);
|
|
||||||
setTitle(savedTrimInfo.title || path.basename(filePath));
|
|
||||||
};
|
|
||||||
|
|
||||||
loadTrimInfo();
|
|
||||||
}, [filePath, section]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (waveformRef.current) {
|
|
||||||
wavesurferRef.current = WaveSurfer.create({
|
|
||||||
container: waveformRef.current,
|
|
||||||
waveColor: '#ccb1ff',
|
|
||||||
progressColor: '#6e44ba',
|
|
||||||
responsive: true,
|
|
||||||
height: 100,
|
|
||||||
hideScrollbar: true,
|
|
||||||
plugins: [
|
|
||||||
RegionsPlugin.create({
|
|
||||||
color: 'rgba(132, 81, 224, 0.3)',
|
|
||||||
drag: false,
|
|
||||||
resize: true,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
wavesurferRef.current.load(`file://${filePath}`);
|
|
||||||
|
|
||||||
wavesurferRef.current.on('ready', () => {
|
|
||||||
wavesurferRef.current?.addRegion({
|
|
||||||
start: trimStart,
|
|
||||||
end: trimEnd,
|
|
||||||
color: 'rgba(132, 81, 224, 0.3)',
|
|
||||||
drag: false,
|
|
||||||
resize: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
wavesurferRef.current.on('region-update-end', (region) => {
|
|
||||||
setTrimStart(region.start);
|
|
||||||
setTrimEnd(region.end);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
wavesurferRef.current?.destroy();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [filePath, trimStart, trimEnd]);
|
|
||||||
|
|
||||||
const handlePlayPause = () => {
|
|
||||||
if (wavesurferRef.current) {
|
|
||||||
if (wavesurferRef.current.isPlaying()) {
|
|
||||||
wavesurferRef.current.pause();
|
|
||||||
} else {
|
|
||||||
wavesurferRef.current.play(trimStart, trimEnd);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveTrim = async () => {
|
|
||||||
const newTitle = title.trim();
|
|
||||||
await ipcRenderer.invoke('save-trimmed-file', {
|
|
||||||
originalFilePath: filePath,
|
|
||||||
trimStart,
|
|
||||||
trimEnd,
|
|
||||||
title: newTitle,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async () => {
|
|
||||||
const confirmDelete = window.confirm('Are you sure you want to delete this audio file?');
|
|
||||||
if (confirmDelete) {
|
|
||||||
await ipcRenderer.invoke('delete-file', filePath);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="audio-trimmer-item" data-filepath={filePath}>
|
|
||||||
<div className="audio-trimmer-header">
|
|
||||||
<div className="audio-trimmer-title-container">
|
|
||||||
<div className="audio-trimmer-title">{title}</div>
|
|
||||||
<div className="audio-trimmer-filename">{path.basename(filePath)}</div>
|
|
||||||
</div>
|
|
||||||
<div className="audio-trimmer-controls">
|
|
||||||
<button className="play-pause-btn" onClick={handlePlayPause}>
|
|
||||||
Play/Pause
|
|
||||||
</button>
|
|
||||||
<button className="save-trim" onClick={handleSaveTrim}>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
<button className="delete-btn" onClick={handleDelete}>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="waveform-container" ref={waveformRef}></div>
|
|
||||||
<div className="trim-info">
|
|
||||||
<div className="trim-time">
|
|
||||||
<span>Start: </span>
|
|
||||||
<span>{formatTime(trimStart)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="trim-time">
|
|
||||||
<span>End: </span>
|
|
||||||
<span>{formatTime(trimEnd)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatTime = (seconds: number) => {
|
|
||||||
const minutes = Math.floor(seconds / 60);
|
|
||||||
const remainingSeconds = Math.floor(seconds % 60);
|
|
||||||
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AudioTrimmer;
|
|
||||||
@ -1,16 +0,0 @@
|
|||||||
// This file is intended for defining TypeScript types and interfaces that can be used throughout the application.
|
|
||||||
|
|
||||||
export interface TrimInfo {
|
|
||||||
title?: string;
|
|
||||||
trimStart: number;
|
|
||||||
trimEnd: number;
|
|
||||||
originalPath: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AudioTrimmerProps {
|
|
||||||
filePath: string;
|
|
||||||
section: string;
|
|
||||||
savedTrimInfo: TrimInfo;
|
|
||||||
onSave: (trimInfo: TrimInfo) => void;
|
|
||||||
onDelete: () => void;
|
|
||||||
}
|
|
||||||
1730
electron-ui/package-lock.json
generated
@ -101,7 +101,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@electron/notarize": "^3.0.0",
|
"@electron/notarize": "^3.0.0",
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
|
"@emotion/styled": "^11.14.1",
|
||||||
|
"@material-tailwind/react": "^2.1.10",
|
||||||
|
"@mui/icons-material": "^7.3.7",
|
||||||
|
"@mui/material": "^7.3.7",
|
||||||
|
"@reduxjs/toolkit": "^2.11.2",
|
||||||
"@tailwindcss/cli": "^4.1.18",
|
"@tailwindcss/cli": "^4.1.18",
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@wavesurfer/react": "^1.0.12",
|
"@wavesurfer/react": "^1.0.12",
|
||||||
@ -110,7 +119,11 @@
|
|||||||
"electron-updater": "^6.3.9",
|
"electron-updater": "^6.3.9",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-redux": "^9.2.0",
|
||||||
"react-router-dom": "^7.3.0",
|
"react-router-dom": "^7.3.0",
|
||||||
|
"socket.io": "^4.8.3",
|
||||||
|
"socket.io-client": "^4.8.3",
|
||||||
|
"socketio": "^1.0.0",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.1.18",
|
||||||
"wavesurfer.js": "^7.12.1"
|
"wavesurfer.js": "^7.12.1"
|
||||||
},
|
},
|
||||||
|
|||||||
5
electron-ui/src/ipc/audio/channels.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
const AudioChannels = {
|
||||||
|
LOAD_AUDIO_BUFFER: 'audio:loadAudioBuffer',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export default AudioChannels;
|
||||||
18
electron-ui/src/ipc/audio/main.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { ipcMain } from 'electron';
|
||||||
|
import fs from 'fs';
|
||||||
|
import AudioChannels from './channels';
|
||||||
|
import { LoadAudioBufferArgs, LoadAudioBufferResult } from './types';
|
||||||
|
|
||||||
|
export default function registerAudioIpcHandlers() {
|
||||||
|
ipcMain.handle(
|
||||||
|
AudioChannels.LOAD_AUDIO_BUFFER,
|
||||||
|
async (_, args: LoadAudioBufferArgs): Promise<LoadAudioBufferResult> => {
|
||||||
|
try {
|
||||||
|
const buffer = await fs.promises.readFile(args.filePath);
|
||||||
|
return { buffer };
|
||||||
|
} catch (err: any) {
|
||||||
|
return { error: err.message };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
8
electron-ui/src/ipc/audio/types.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export interface LoadAudioBufferArgs {
|
||||||
|
filePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoadAudioBufferResult {
|
||||||
|
buffer?: Buffer;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
8
electron-ui/src/ipc/settings/channels.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const SettingsChannels = {
|
||||||
|
GET_DEFAULTS: 'settings:get-defaults',
|
||||||
|
GET_SETTINGS: 'settings:get-settings',
|
||||||
|
SET_SETTINGS: 'settings:set-settings',
|
||||||
|
GET_INPUT_DEVICES: 'settings:get-input-devices',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export default SettingsChannels;
|
||||||
@ -15,6 +15,7 @@ import { autoUpdater } from 'electron-updater';
|
|||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
import MenuBuilder from './menu';
|
import MenuBuilder from './menu';
|
||||||
import { resolveHtmlPath } from './util';
|
import { resolveHtmlPath } from './util';
|
||||||
|
import registerFileIpcHandlers from '../ipc/audio/main';
|
||||||
|
|
||||||
class AppUpdater {
|
class AppUpdater {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -108,17 +109,7 @@ const createWindow = async () => {
|
|||||||
return { action: 'deny' };
|
return { action: 'deny' };
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('load-audio-buffer', async (event, filePath) => {
|
registerFileIpcHandlers();
|
||||||
try {
|
|
||||||
// console.log(`Loading audio file: ${filePath}`);
|
|
||||||
const buffer = fs.readFileSync(filePath);
|
|
||||||
// console.log(buffer);
|
|
||||||
return buffer;
|
|
||||||
} catch (err) {
|
|
||||||
return { error: err.message };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove this if your app does not use auto updates
|
// Remove this if your app does not use auto updates
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
new AppUpdater();
|
new AppUpdater();
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
// Disable no-unused-vars, broken for spread args
|
// Disable no-unused-vars, broken for spread args
|
||||||
/* eslint no-unused-vars: off */
|
/* eslint no-unused-vars: off */
|
||||||
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';
|
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';
|
||||||
|
import FileChannels from '../ipc/audio/channels';
|
||||||
|
import { LoadAudioBufferArgs, ReadTextArgs } from '../ipc/audio/types';
|
||||||
|
import AudioChannels from '../ipc/audio/channels';
|
||||||
|
// import '../ipc/file/preload'; // Import file API preload to ensure it runs and exposes the API
|
||||||
|
|
||||||
export type Channels = 'ipc-example';
|
export type Channels = 'ipc-example';
|
||||||
|
|
||||||
@ -22,11 +26,27 @@ const electronHandler = {
|
|||||||
ipcRenderer.once(channel, (_event, ...args) => func(...args));
|
ipcRenderer.once(channel, (_event, ...args) => func(...args));
|
||||||
},
|
},
|
||||||
|
|
||||||
loadAudioBuffer: (filePath: string) =>
|
invoke: (event: string, ...args: unknown[]) =>
|
||||||
ipcRenderer.invoke('load-audio-buffer', filePath),
|
ipcRenderer.invoke(event, ...args),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('electron', electronHandler);
|
contextBridge.exposeInMainWorld('electron', electronHandler);
|
||||||
|
|
||||||
export type ElectronHandler = typeof electronHandler;
|
export type ElectronHandler = typeof electronHandler;
|
||||||
|
|
||||||
|
const audioHandler = {
|
||||||
|
loadAudioBuffer: (filePath: string) =>
|
||||||
|
ipcRenderer.invoke(AudioChannels.LOAD_AUDIO_BUFFER, {
|
||||||
|
filePath,
|
||||||
|
} satisfies LoadAudioBufferArgs),
|
||||||
|
|
||||||
|
readText: (filePath: string) =>
|
||||||
|
ipcRenderer.invoke(AudioChannels.READ_TEXT, {
|
||||||
|
filePath,
|
||||||
|
} satisfies ReadTextArgs),
|
||||||
|
};
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('audio', audioHandler);
|
||||||
|
|
||||||
|
export type AudioHandler = typeof audioHandler;
|
||||||
|
|||||||
@ -1,74 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Audio Clip Trimmer</title>
|
|
||||||
<link rel="stylesheet" href="styles.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="titlebar"></div>
|
|
||||||
<div class="app-container">
|
|
||||||
<div class="sidebar">
|
|
||||||
<div class="sidebar-section">
|
|
||||||
<h3>Collections</h3>
|
|
||||||
<div id="collections-list"></div>
|
|
||||||
<button id="add-collection-btn" class="add-collection-btn">
|
|
||||||
+ New Collection
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="sidebar-section">
|
|
||||||
<div id="nav-buttons">
|
|
||||||
<button id="settings-btn" class="nav-btn">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.06-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.06,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button id="restart-btn" class="nav-btn">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
||||||
<path
|
|
||||||
d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="main-content">
|
|
||||||
<div class="audio-trimmers-section">
|
|
||||||
<div id="audio-trimmers-list" class="audio-trimmers-list">
|
|
||||||
<!-- Audio trimmers will be dynamically added here -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Settings Modal -->
|
|
||||||
<div id="settings-modal" class="modal">
|
|
||||||
<div class="modal-content">
|
|
||||||
<span class="close-modal">×</span>
|
|
||||||
<h2>Settings</h2>
|
|
||||||
<div class="settings-group">
|
|
||||||
<label for="recording-length">Recording Length (seconds):</label>
|
|
||||||
<input type="number" id="recording-length" min="1" max="300" />
|
|
||||||
</div>
|
|
||||||
<div class="settings-group">
|
|
||||||
<label for="osc-port">OSC port:</label>
|
|
||||||
<input type="number" id="osc-port" min="5000" max="6000" />
|
|
||||||
</div>
|
|
||||||
<div class="settings-group">
|
|
||||||
<label for="output-folder">Output Folder:</label>
|
|
||||||
<input type="text" id="output-folder" readonly />
|
|
||||||
<button id="select-output-folder">Browse</button>
|
|
||||||
</div>
|
|
||||||
<div class="settings-group">
|
|
||||||
<label for="input-device">Input Device:</label>
|
|
||||||
<select id="input-device"></select>
|
|
||||||
</div>
|
|
||||||
<button id="save-settings">Save Settings</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script src="node_modules/wavesurfer.js/dist/wavesurfer.min.js"></script>
|
|
||||||
<script src="node_modules/wavesurfer.js/dist/plugin/wavesurfer.regions.js"></script>
|
|
||||||
<script src="renderer.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@ -1,483 +0,0 @@
|
|||||||
const { app, BrowserWindow, ipcMain, Tray, Menu, dialog } = require('electron');
|
|
||||||
const path = require('path');
|
|
||||||
const os = require('os');
|
|
||||||
const spawn = require('child_process').spawn;
|
|
||||||
require('electron-reload')(__dirname);
|
|
||||||
const fs = require('fs').promises;
|
|
||||||
const chokidar = require('chokidar');
|
|
||||||
const wavefile = require('wavefile');
|
|
||||||
const MetadataManager = require('./metatadata');
|
|
||||||
|
|
||||||
const { webContents } = require('electron');
|
|
||||||
|
|
||||||
// import { app, BrowserWindow, ipcMain, Tray, Menu, dialog } from "electron";
|
|
||||||
// import path from "path";
|
|
||||||
// import os from "os";
|
|
||||||
// import spawn from 'child_process';
|
|
||||||
// import fs from "fs";
|
|
||||||
// import chokidar from "chokidar";
|
|
||||||
// import wavefile from "wavefile";
|
|
||||||
// import MetadataManager from "./metatadata.cjs";
|
|
||||||
// import { webContents } from "electron";
|
|
||||||
|
|
||||||
let mainWindow;
|
|
||||||
let tray;
|
|
||||||
let audioServiceProcess;
|
|
||||||
|
|
||||||
const metadataPath = path.join(app.getPath('userData'), 'audio_metadata.json');
|
|
||||||
const metadataManager = new MetadataManager(metadataPath);
|
|
||||||
|
|
||||||
async function createPythonService() {
|
|
||||||
const pythonPath =
|
|
||||||
process.platform === 'win32'
|
|
||||||
? path.join(
|
|
||||||
__dirname,
|
|
||||||
'..',
|
|
||||||
'..',
|
|
||||||
'audio-service',
|
|
||||||
'venv',
|
|
||||||
'Scripts',
|
|
||||||
'python.exe',
|
|
||||||
)
|
|
||||||
: path.join(__dirname, '..', 'audio-service', 'venv', 'bin', 'python');
|
|
||||||
|
|
||||||
const scriptPath = path.join(
|
|
||||||
__dirname,
|
|
||||||
'..',
|
|
||||||
'..',
|
|
||||||
'audio-service',
|
|
||||||
'src',
|
|
||||||
'main.py',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Load settings to pass as arguments
|
|
||||||
const settings = await loadSettings(); // Assuming you have a method to load settings
|
|
||||||
|
|
||||||
const args = [
|
|
||||||
scriptPath,
|
|
||||||
'--recording-length',
|
|
||||||
settings.recordingLength.toString(),
|
|
||||||
'--save-path',
|
|
||||||
path.join(settings.outputFolder, 'original'),
|
|
||||||
'--osc-port',
|
|
||||||
settings.oscPort.toString(), // Or make this configurable
|
|
||||||
];
|
|
||||||
|
|
||||||
// Add input device if specified
|
|
||||||
if (settings.inputDevice) {
|
|
||||||
const devices = await listAudioDevices();
|
|
||||||
args.push(
|
|
||||||
'--input-device',
|
|
||||||
devices.find((device) => device.id === settings.inputDevice)?.name,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(args);
|
|
||||||
|
|
||||||
audioServiceProcess = spawn(pythonPath, args, {
|
|
||||||
detached: false,
|
|
||||||
stdio: 'pipe',
|
|
||||||
});
|
|
||||||
|
|
||||||
audioServiceProcess.stdout.on('data', (data) => {
|
|
||||||
console.log(`Audio Service: ${data}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
audioServiceProcess.stderr.on('data', (data) => {
|
|
||||||
console.error(`Audio Service Error: ${data}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
audioServiceProcess.on('close', (code) => {
|
|
||||||
console.log(`Audio Service process exited with code ${code}`);
|
|
||||||
audioServiceProcess = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function createTray() {
|
|
||||||
tray = new Tray(path.join(__dirname, 'assets', 'tray-icon.png')); // You'll need to create this icon
|
|
||||||
|
|
||||||
const contextMenu = Menu.buildFromTemplate([
|
|
||||||
{
|
|
||||||
label: 'Show',
|
|
||||||
click: () => {
|
|
||||||
mainWindow.show();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Quit',
|
|
||||||
click: () => {
|
|
||||||
// Properly terminate the Python service
|
|
||||||
|
|
||||||
stopService();
|
|
||||||
app.quit();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
tray.setToolTip('Audio Trimmer');
|
|
||||||
tray.setContextMenu(contextMenu);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkNewWavFile(filePath) {
|
|
||||||
// Only process .wav files
|
|
||||||
if (path.extname(filePath).toLowerCase() === '.wav') {
|
|
||||||
try {
|
|
||||||
await metadataManager.addUntrimmedFile(filePath);
|
|
||||||
|
|
||||||
// Notify renderer if window is ready
|
|
||||||
if (mainWindow) {
|
|
||||||
mainWindow.webContents.send('new-untrimmed-file', filePath);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error adding untrimmed file:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function stopService() {
|
|
||||||
if (audioServiceProcess) {
|
|
||||||
try {
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
spawn('taskkill', ['/pid', audioServiceProcess.pid, '/f', '/t']);
|
|
||||||
} else {
|
|
||||||
audioServiceProcess.kill('SIGTERM');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error killing audio service:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function restartService() {
|
|
||||||
// Properly terminate the Python service
|
|
||||||
stopService();
|
|
||||||
//delay for 2 seconds
|
|
||||||
setTimeout(createPythonService, 4000);
|
|
||||||
//createPythonService();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadSettings() {
|
|
||||||
try {
|
|
||||||
const settingsPath = path.join(app.getPath('userData'), 'settings.json');
|
|
||||||
const settingsData = await fs.readFile(settingsPath, 'utf8');
|
|
||||||
return JSON.parse(settingsData);
|
|
||||||
} catch (error) {
|
|
||||||
// If no settings file exists, return default settings
|
|
||||||
return {
|
|
||||||
recordingLength: 30,
|
|
||||||
outputFolder: path.join(os.homedir(), 'AudioTrimmer'),
|
|
||||||
inputDevice: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function listAudioDevices() {
|
|
||||||
try {
|
|
||||||
// Use a webContents to access navigator.mediaDevices
|
|
||||||
|
|
||||||
const contents = webContents.getAllWebContents()[0];
|
|
||||||
|
|
||||||
const devices = await contents.executeJavaScript(`
|
|
||||||
navigator.mediaDevices.enumerateDevices()
|
|
||||||
.then(devices => devices.filter(device => device.kind === 'audioinput'))
|
|
||||||
.then(audioDevices => audioDevices.map(device => ({
|
|
||||||
id: device.deviceId,
|
|
||||||
name: device.label || 'Unknown Microphone'
|
|
||||||
})))
|
|
||||||
`);
|
|
||||||
|
|
||||||
return devices;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting input devices:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function createWindow() {
|
|
||||||
// Initialize metadata
|
|
||||||
await metadataManager.initialize();
|
|
||||||
|
|
||||||
mainWindow = new BrowserWindow({
|
|
||||||
width: 1200,
|
|
||||||
height: 800,
|
|
||||||
autoHideMenuBar: true,
|
|
||||||
titleBarStyle: 'hidden',
|
|
||||||
frame: false,
|
|
||||||
|
|
||||||
// titleBarOverlay: {
|
|
||||||
// color: '#1e1e1e',
|
|
||||||
// symbolColor: '#ffffff',
|
|
||||||
// height: 30
|
|
||||||
// },
|
|
||||||
webPreferences: {
|
|
||||||
nodeIntegration: true,
|
|
||||||
contextIsolation: false,
|
|
||||||
// Add these to help with graphics issues
|
|
||||||
},
|
|
||||||
// These additional options can help with graphics rendering
|
|
||||||
backgroundColor: '#1e1e1e',
|
|
||||||
...(process.platform !== 'darwin'
|
|
||||||
? {
|
|
||||||
titleBarOverlay: {
|
|
||||||
color: '#262626',
|
|
||||||
symbolColor: '#ffffff',
|
|
||||||
height: 30,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
});
|
|
||||||
mainWindow.loadFile('src/index.html');
|
|
||||||
|
|
||||||
// Create Python ser
|
|
||||||
const settings = await loadSettings(); // Assuming you have a method to load settings
|
|
||||||
const recordingsPath = path.join(settings.outputFolder, 'original');
|
|
||||||
// Ensure recordings directory exists
|
|
||||||
try {
|
|
||||||
await fs.mkdir(recordingsPath, { recursive: true });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error creating recordings directory:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Watch for new WAV files
|
|
||||||
const watcher = chokidar.watch(recordingsPath, {
|
|
||||||
ignored: /(^|[\/\\])\../, // ignore dotfiles
|
|
||||||
persistent: true,
|
|
||||||
depth: 0,
|
|
||||||
awaitWriteFinish: {
|
|
||||||
stabilityThreshold: 2000,
|
|
||||||
pollInterval: 100,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
fs.readdir(recordingsPath).then((files) => {
|
|
||||||
files.forEach((file) => {
|
|
||||||
checkNewWavFile(path.join(recordingsPath, file));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
watcher.on('add', async (filePath) => {
|
|
||||||
await checkNewWavFile(filePath);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('get-collections', () => {
|
|
||||||
return metadataManager.getCollections();
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('get-collection-files', (event, collectionPath) => {
|
|
||||||
return metadataManager.getFilesInCollection(collectionPath);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('add-untrimmed-file', (event, filePath) => {
|
|
||||||
return metadataManager.addUntrimmedFile(filePath);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle(
|
|
||||||
'save-trimmed-file',
|
|
||||||
(event, fileName, previousPath, savePath, trimStart, trimEnd, title) => {
|
|
||||||
return metadataManager.saveTrimmedFile(
|
|
||||||
fileName,
|
|
||||||
previousPath,
|
|
||||||
savePath,
|
|
||||||
trimStart,
|
|
||||||
trimEnd,
|
|
||||||
title,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
ipcMain.handle('restart', (event) => {
|
|
||||||
restartService();
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('delete-old-file', (event, outputFolder, section, title) => {
|
|
||||||
if (section === 'untrimmed') return;
|
|
||||||
const collectionPath = path.join(outputFolder, section);
|
|
||||||
const outputFilePath = path.join(collectionPath, `${title}.wav`);
|
|
||||||
fs.unlink(outputFilePath);
|
|
||||||
});
|
|
||||||
ipcMain.handle(
|
|
||||||
'save-trimmed-audio',
|
|
||||||
async (
|
|
||||||
event,
|
|
||||||
{
|
|
||||||
originalFilePath,
|
|
||||||
outputFolder,
|
|
||||||
collectionName,
|
|
||||||
title,
|
|
||||||
trimStart,
|
|
||||||
trimEnd,
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
// Ensure the collection folder exists
|
|
||||||
const collectionPath = path.join(outputFolder, collectionName);
|
|
||||||
await fs.mkdir(collectionPath, { recursive: true });
|
|
||||||
|
|
||||||
// Generate output file path
|
|
||||||
const outputFilePath = path.join(collectionPath, `${title}.wav`);
|
|
||||||
|
|
||||||
// Read the original WAV file
|
|
||||||
const originalWaveFile = new wavefile.WaveFile(
|
|
||||||
await fs.readFile(originalFilePath),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Calculate trim points in samples
|
|
||||||
const sampleRate = originalWaveFile.fmt.sampleRate;
|
|
||||||
const startSample = Math.floor(trimStart * sampleRate);
|
|
||||||
const endSample = Math.floor(trimEnd * sampleRate);
|
|
||||||
|
|
||||||
// Extract trimmed audio samples
|
|
||||||
const originalSamples = originalWaveFile.getSamples(false);
|
|
||||||
const trimmedSamples = [
|
|
||||||
originalSamples[0].slice(startSample, endSample),
|
|
||||||
originalSamples[1].slice(startSample, endSample),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Normalize samples if they are Int16 or Int32
|
|
||||||
let normalizedSamples;
|
|
||||||
const bitDepth = originalWaveFile.fmt.bitsPerSample;
|
|
||||||
|
|
||||||
// if (bitDepth === 16) {
|
|
||||||
// // For 16-bit audio, convert to Float32
|
|
||||||
// normalizedSamples = [new Float32Array(trimmedSamples[0].length),new Float32Array(trimmedSamples[0].length)];
|
|
||||||
// for (let i = 0; i < trimmedSamples[0].length; i++) {
|
|
||||||
// normalizedSamples[0][i] = trimmedSamples[0][i] / 32768.0;
|
|
||||||
// normalizedSamples[1][i] = trimmedSamples[1][i] / 32768.0;
|
|
||||||
// }
|
|
||||||
// } else if (bitDepth === 32) {
|
|
||||||
// // For 32-bit float audio, just convert to Float32
|
|
||||||
// normalizedSamples = [new Float32Array(trimmedSamples[0]),new Float32Array(trimmedSamples[1])];
|
|
||||||
// } else {
|
|
||||||
// throw new Error(`Unsupported bit depth: ${bitDepth}`);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Create a new WaveFile with normalized samples
|
|
||||||
const trimmedWaveFile = new wavefile.WaveFile();
|
|
||||||
trimmedWaveFile.fromScratch(
|
|
||||||
originalWaveFile.fmt.numChannels,
|
|
||||||
sampleRate,
|
|
||||||
bitDepth, // Always use 32-bit float
|
|
||||||
trimmedSamples,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Write the trimmed WAV file
|
|
||||||
await fs.writeFile(outputFilePath, trimmedWaveFile.toBuffer());
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
filePath: outputFilePath,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving trimmed audio:', error);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: error.message,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
ipcMain.handle('delete-file', async (event, filePath) => {
|
|
||||||
try {
|
|
||||||
const settings = await loadSettings();
|
|
||||||
return metadataManager.deletefile(filePath, settings.outputFolder);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error Deleting file:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('add-new-collection', (event, collectionName) => {
|
|
||||||
try {
|
|
||||||
return metadataManager.addNewCollection(collectionName);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error adding collection:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ipcMain.handle('get-trim-info', (event, collectionName, filePath) => {
|
|
||||||
return metadataManager.getTrimInfo(collectionName, filePath);
|
|
||||||
});
|
|
||||||
ipcMain.handle(
|
|
||||||
'set-trim-info',
|
|
||||||
(event, collectionName, filePath, trim_info) => {
|
|
||||||
return metadataManager.setTrimInfo(collectionName, filePath, trim_info);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add these IPC handlers
|
|
||||||
ipcMain.handle('select-output-folder', async (event) => {
|
|
||||||
const result = await dialog.showOpenDialog({
|
|
||||||
properties: ['openDirectory'],
|
|
||||||
});
|
|
||||||
return result.filePaths[0] || '';
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('get-default-settings', () => {
|
|
||||||
return {
|
|
||||||
recordingLength: 30,
|
|
||||||
outputFolder: path.join(os.homedir(), 'AudioTrimmer'),
|
|
||||||
inputDevice: null,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('save-settings', async (event, settings) => {
|
|
||||||
try {
|
|
||||||
// Ensure output folder exists
|
|
||||||
await fs.mkdir(settings.outputFolder, { recursive: true });
|
|
||||||
|
|
||||||
// Save settings to a file
|
|
||||||
const settingsPath = path.join(app.getPath('userData'), 'settings.json');
|
|
||||||
await fs.writeFile(settingsPath, JSON.stringify(settings, null, 2));
|
|
||||||
restartService();
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving settings:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('load-settings', async () => {
|
|
||||||
return loadSettings();
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle('get-input-devices', async () => {
|
|
||||||
return await listAudioDevices();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Minimize to tray instead of closing
|
|
||||||
mainWindow.on('close', (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
mainWindow.hide();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create system tray
|
|
||||||
createTray();
|
|
||||||
|
|
||||||
// Launch Python audio service
|
|
||||||
createPythonService();
|
|
||||||
}
|
|
||||||
app.disableHardwareAcceleration();
|
|
||||||
app.whenReady().then(createWindow);
|
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
|
||||||
// Do nothing - we handle closing via tray
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ensure Python service is killed when app quits
|
|
||||||
app.on('before-quit', () => {
|
|
||||||
if (audioServiceProcess) {
|
|
||||||
try {
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
spawn('taskkill', ['/pid', audioServiceProcess.pid, '/f', '/t']);
|
|
||||||
} else {
|
|
||||||
audioServiceProcess.kill('SIGTERM');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error killing audio service:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
app.on('activate', () => {
|
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
|
||||||
createWindow();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@ -1,234 +0,0 @@
|
|||||||
const fs = require('fs').promises;
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
// import fs from 'fs';
|
|
||||||
// import path from 'path';
|
|
||||||
|
|
||||||
class MetadataManager {
|
|
||||||
constructor(metadataPath) {
|
|
||||||
this.metadataPath = metadataPath;
|
|
||||||
this.metadata = {};
|
|
||||||
//this.initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
async initialize() {
|
|
||||||
try {
|
|
||||||
// Create metadata file if it doesn't exist
|
|
||||||
console.log(this.metadataPath);
|
|
||||||
await this.ensureMetadataFileExists();
|
|
||||||
|
|
||||||
// Load existing metadata
|
|
||||||
const rawData = await fs.readFile(this.metadataPath, 'utf8');
|
|
||||||
this.metadata = JSON.parse(rawData);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error initializing metadata:', error);
|
|
||||||
this.metadata = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async ensureMetadataFileExists() {
|
|
||||||
try {
|
|
||||||
await fs.access(this.metadataPath);
|
|
||||||
} catch (error) {
|
|
||||||
// File doesn't exist, create it with an empty object
|
|
||||||
await fs.writeFile(this.metadataPath, JSON.stringify({
|
|
||||||
collections: {
|
|
||||||
untrimmed: {}
|
|
||||||
}
|
|
||||||
}, null, 2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async addUntrimmedFile(filePath) {
|
|
||||||
try {
|
|
||||||
// Read existing metadata
|
|
||||||
const metadata = this.metadata;
|
|
||||||
|
|
||||||
// Check if file is already in untrimmed files
|
|
||||||
const fileName = path.basename(filePath);
|
|
||||||
const existingUntrimmedFiles = Object.keys(metadata.collections.untrimmed) || [];
|
|
||||||
|
|
||||||
// Check if the file is already in trimmed files across all collections
|
|
||||||
const collections = Object.keys(metadata.collections || {});
|
|
||||||
const isAlreadyTrimmed = collections.some(collection => {
|
|
||||||
return (Object.keys(metadata.collections[collection] || {})).some(name => {
|
|
||||||
return fileName === name;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// If already trimmed, don't add to untrimmed files
|
|
||||||
if (isAlreadyTrimmed) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent duplicates
|
|
||||||
if (!existingUntrimmedFiles.includes(filePath)) {
|
|
||||||
const d = new Date()
|
|
||||||
metadata.collections.untrimmed[fileName] = {
|
|
||||||
originalPath:filePath,
|
|
||||||
addedAt:d.toISOString()
|
|
||||||
}
|
|
||||||
// Write updated metadata
|
|
||||||
await this.saveMetadata();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error adding untrimmed file:', error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async saveTrimmedFile(fileName, previousPath, savePath, trimStart, trimEnd, title) {
|
|
||||||
console.log(title);
|
|
||||||
// Ensure collection exists
|
|
||||||
if (!this.metadata.collections[savePath]) {
|
|
||||||
this.metadata.collections[savePath] = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the original untrimmed file
|
|
||||||
const original = this.metadata.collections[previousPath][fileName];
|
|
||||||
|
|
||||||
// Add to specified collection
|
|
||||||
this.metadata.collections[savePath][fileName] = {
|
|
||||||
...original,
|
|
||||||
title,
|
|
||||||
trimStart,
|
|
||||||
trimEnd,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// Remove from untrimmed if it exists
|
|
||||||
if(previousPath !== savePath) {
|
|
||||||
// if(previousPath !== 'untrimmed') {
|
|
||||||
// const prevmeta = this.metadata.collections[previousPath][fileName];
|
|
||||||
// let delete_path = path.concat(previousPath, prevmeta.title + ".wav");
|
|
||||||
// }
|
|
||||||
delete this.metadata.collections[previousPath][fileName];
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.saveMetadata();
|
|
||||||
|
|
||||||
return fileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
async saveMetadata() {
|
|
||||||
try {
|
|
||||||
await fs.writeFile(
|
|
||||||
this.metadataPath,
|
|
||||||
JSON.stringify(this.metadata, null, 2)
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error saving metadata:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async getUntrimmedFiles() {
|
|
||||||
try {
|
|
||||||
// Read the metadata file
|
|
||||||
const metadata = await this.readMetadataFile();
|
|
||||||
|
|
||||||
// Get all collections
|
|
||||||
const collections = Object.keys(metadata.collections || {});
|
|
||||||
|
|
||||||
// Collect all trimmed file names across all collections
|
|
||||||
const trimmedFiles = new Set();
|
|
||||||
collections.forEach(collection => {
|
|
||||||
const collectionTrimmedFiles = metadata.collections[collection]?.trimmedFiles || [];
|
|
||||||
collectionTrimmedFiles.forEach(trimmedFile => {
|
|
||||||
trimmedFiles.add(trimmedFile.originalFileName);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Filter out untrimmed files that have been trimmed
|
|
||||||
const untrimmedFiles = (metadata.untrimmedFiles || []).filter(file =>
|
|
||||||
!trimmedFiles.has(path.basename(file))
|
|
||||||
);
|
|
||||||
|
|
||||||
return untrimmedFiles;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting untrimmed files:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async deletefile(filePath, collectionPath) {
|
|
||||||
try {
|
|
||||||
const fileName = path.basename(filePath);
|
|
||||||
for (const collection in this.metadata.collections) {
|
|
||||||
if (this.metadata.collections[collection][fileName]) {
|
|
||||||
let delete_path = this.metadata.collections[collection][fileName].originalPath;
|
|
||||||
fs.unlink(delete_path);
|
|
||||||
if(collection !== 'untrimmed') {
|
|
||||||
delete_path = path.join(collectionPath, collection, this.metadata.collections[collection][fileName].title + ".wav");
|
|
||||||
fs.unlink(delete_path);
|
|
||||||
}
|
|
||||||
delete this.metadata.collections[collection][fileName];
|
|
||||||
this.saveMetadata();
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
getCollections() {
|
|
||||||
return Object.keys(this.metadata.collections);
|
|
||||||
}
|
|
||||||
|
|
||||||
getTrimInfo(collectionName, filePath) {
|
|
||||||
return this.metadata.collections[collectionName][filePath] || {
|
|
||||||
trimStart: 0,
|
|
||||||
trimEnd: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
setTrimInfo(collectionName, filePath, trimInfo) {
|
|
||||||
this.metadata.collections[collectionName][filePath].trimStart = trimInfo.trimStart;
|
|
||||||
this.metadata.collections[collectionName][filePath].trimEnd = trimInfo.trimEnd;
|
|
||||||
this.saveMetadata();
|
|
||||||
}
|
|
||||||
|
|
||||||
getFilesInCollection(collectionPath) {
|
|
||||||
// if(collectionPath === 'untrimmed') {
|
|
||||||
// return Object.keys(this.metadata.untrimmed).map(fileName => ({
|
|
||||||
// fileName,
|
|
||||||
// ...this.metadata.untrimmed[fileName]
|
|
||||||
// }));
|
|
||||||
// }
|
|
||||||
return Object.keys(this.metadata.collections[collectionPath] || {}).map(fileName => {
|
|
||||||
const fileInfo = this.metadata.collections[collectionPath][fileName];
|
|
||||||
return {
|
|
||||||
fileName,
|
|
||||||
...this.metadata.collections[collectionPath][fileName],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async addNewCollection(collectionName) {
|
|
||||||
// Ensure collection name is valid
|
|
||||||
if (!collectionName || collectionName.trim() === '') {
|
|
||||||
throw new Error('Collection name cannot be empty');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Normalize collection name (remove leading/trailing spaces, convert to lowercase)
|
|
||||||
const normalizedName = collectionName.trim().toLowerCase();
|
|
||||||
|
|
||||||
// Check if collection already exists
|
|
||||||
if (this.metadata.collections[normalizedName]) {
|
|
||||||
throw new Error(`Collection '${normalizedName}' already exists`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new collection
|
|
||||||
this.metadata.collections[normalizedName] = {};
|
|
||||||
|
|
||||||
// Save updated metadata
|
|
||||||
await this.saveMetadata();
|
|
||||||
|
|
||||||
return normalizedName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = MetadataManager;
|
|
||||||
@ -1,818 +0,0 @@
|
|||||||
const { ipcRenderer } = require('electron');
|
|
||||||
// const path = require('path');
|
|
||||||
const WaveSurfer = require('wavesurfer.js');
|
|
||||||
const RegionsPlugin = require('wavesurfer.js/dist/plugin/wavesurfer.regions.js');
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
|
||||||
// Settings Modal Logic
|
|
||||||
const settingsModal = document.getElementById('settings-modal');
|
|
||||||
const settingsBtn = document.getElementById('settings-btn');
|
|
||||||
const restartBtn = document.getElementById('restart-btn');
|
|
||||||
const closeModalBtn = document.querySelector('.close-modal');
|
|
||||||
const saveSettingsBtn = document.getElementById('save-settings');
|
|
||||||
const selectOutputFolderBtn = document.getElementById('select-output-folder');
|
|
||||||
const recordingLengthInput = document.getElementById('recording-length');
|
|
||||||
const oscPortInput = document.getElementById('osc-port');
|
|
||||||
const outputFolderInput = document.getElementById('output-folder');
|
|
||||||
const inputDeviceSelect = document.getElementById('input-device');
|
|
||||||
|
|
||||||
// Open settings modal
|
|
||||||
settingsBtn.addEventListener('click', async () => {
|
|
||||||
try {
|
|
||||||
// Request microphone permissions first
|
|
||||||
await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
||||||
|
|
||||||
// Load current settings
|
|
||||||
const settings = await ipcRenderer.invoke('load-settings');
|
|
||||||
|
|
||||||
// Populate input devices
|
|
||||||
const devices = await ipcRenderer.invoke('get-input-devices');
|
|
||||||
|
|
||||||
if (devices.length === 0) {
|
|
||||||
inputDeviceSelect.innerHTML = '<option>No microphones found</option>';
|
|
||||||
} else {
|
|
||||||
inputDeviceSelect.innerHTML = devices
|
|
||||||
.map(
|
|
||||||
(device) => `<option value="${device.id}">${device.name}</option>`,
|
|
||||||
)
|
|
||||||
.join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set current settings
|
|
||||||
recordingLengthInput.value = settings.recordingLength;
|
|
||||||
outputFolderInput.value = settings.outputFolder;
|
|
||||||
inputDeviceSelect.value = settings.inputDevice;
|
|
||||||
oscPortInput.value = settings.oscPort;
|
|
||||||
|
|
||||||
settingsModal.style.display = 'block';
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading settings or devices:', error);
|
|
||||||
alert('Please grant microphone permissions to list audio devices');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
restartBtn.addEventListener('click', async () => {
|
|
||||||
try {
|
|
||||||
await ipcRenderer.invoke('restart');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error restarting:', error);
|
|
||||||
alert('Failed to restart Clipper');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close settings modal
|
|
||||||
closeModalBtn.addEventListener('click', () => {
|
|
||||||
settingsModal.style.display = 'none';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Select output folder
|
|
||||||
selectOutputFolderBtn.addEventListener('click', async () => {
|
|
||||||
const folderPath = await ipcRenderer.invoke('select-output-folder');
|
|
||||||
if (folderPath) {
|
|
||||||
outputFolderInput.value = folderPath;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Save settings
|
|
||||||
saveSettingsBtn.addEventListener('click', async () => {
|
|
||||||
const settings = {
|
|
||||||
recordingLength: parseInt(recordingLengthInput.value),
|
|
||||||
oscPort: parseInt(oscPortInput.value),
|
|
||||||
outputFolder: outputFolderInput.value,
|
|
||||||
inputDevice: inputDeviceSelect.value,
|
|
||||||
};
|
|
||||||
|
|
||||||
const saved = await ipcRenderer.invoke('save-settings', settings);
|
|
||||||
if (saved) {
|
|
||||||
settingsModal.style.display = 'none';
|
|
||||||
} else {
|
|
||||||
alert('Failed to save settings');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close modal if clicked outside
|
|
||||||
window.addEventListener('click', (event) => {
|
|
||||||
if (event.target === settingsModal) {
|
|
||||||
settingsModal.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const audioTrimmersList = document.getElementById('audio-trimmers-list');
|
|
||||||
const collectionsList = document.getElementById('collections-list');
|
|
||||||
//const currentSectionTitle = document.getElementById("current-section-title");
|
|
||||||
|
|
||||||
// Global state to persist wavesurfer instances and trimmer states
|
|
||||||
const globalState = {
|
|
||||||
wavesurferInstances: {},
|
|
||||||
trimmerStates: {},
|
|
||||||
currentSection: 'untrimmed',
|
|
||||||
trimmerElements: {},
|
|
||||||
};
|
|
||||||
// Utility function to format time
|
|
||||||
function formatTime(seconds) {
|
|
||||||
const minutes = Math.floor(seconds / 60);
|
|
||||||
const remainingSeconds = Math.floor(seconds % 60);
|
|
||||||
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Populate collections list
|
|
||||||
async function populateCollectionsList() {
|
|
||||||
const collections = await ipcRenderer.invoke('get-collections');
|
|
||||||
|
|
||||||
collectionsList.innerHTML = '';
|
|
||||||
|
|
||||||
// Always add Untrimmed section first
|
|
||||||
const untrimmedItem = document.createElement('div');
|
|
||||||
untrimmedItem.classList.add('collection-item');
|
|
||||||
untrimmedItem.textContent = 'Untrimmed';
|
|
||||||
untrimmedItem.dataset.collection = 'untrimmed';
|
|
||||||
|
|
||||||
untrimmedItem.addEventListener('click', () => {
|
|
||||||
loadCollectionFiles('untrimmed');
|
|
||||||
});
|
|
||||||
|
|
||||||
collectionsList.appendChild(untrimmedItem);
|
|
||||||
|
|
||||||
// Add other collections
|
|
||||||
collections.forEach((collection) => {
|
|
||||||
if (collection === 'untrimmed') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const collectionItem = document.createElement('div');
|
|
||||||
collectionItem.classList.add('collection-item');
|
|
||||||
collectionItem.textContent = collection;
|
|
||||||
collectionItem.dataset.collection = collection;
|
|
||||||
|
|
||||||
collectionItem.addEventListener('click', () => {
|
|
||||||
loadCollectionFiles(collection);
|
|
||||||
});
|
|
||||||
|
|
||||||
collectionsList.appendChild(collectionItem);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Modify loadCollectionFiles function
|
|
||||||
async function loadCollectionFiles(collection) {
|
|
||||||
if (collection !== globalState.currentSection) {
|
|
||||||
//Clear existing trimmers and reset global state
|
|
||||||
Object.keys(globalState.trimmerElements).forEach((filePath) => {
|
|
||||||
const trimmerElement = globalState.trimmerElements[filePath];
|
|
||||||
if (trimmerElement && trimmerElement.parentNode) {
|
|
||||||
trimmerElement.parentNode.removeChild(trimmerElement);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset global state
|
|
||||||
globalState.trimmerElements = {};
|
|
||||||
globalState.wavesurferInstances = {};
|
|
||||||
globalState.trimmerStates = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset active states
|
|
||||||
document.querySelectorAll('.collection-item').forEach((el) => {
|
|
||||||
el.classList.remove('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set active state only for existing items
|
|
||||||
const activeItem = document.querySelector(
|
|
||||||
`.collection-item[data-collection="${collection}"]`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Only add active class if the item exists
|
|
||||||
if (activeItem) {
|
|
||||||
activeItem.classList.add('active');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update section title and global state
|
|
||||||
//currentSectionTitle.textContent = collection;
|
|
||||||
globalState.currentSection = collection;
|
|
||||||
|
|
||||||
// Load files
|
|
||||||
const files = await ipcRenderer.invoke('get-collection-files', collection);
|
|
||||||
|
|
||||||
// Add new trimmers with saved trim information
|
|
||||||
for (const file of files) {
|
|
||||||
const filePath = file.originalPath || file.fileName;
|
|
||||||
|
|
||||||
// If loading a collection, use saved trim information
|
|
||||||
//if (collection !== "untrimmed") {
|
|
||||||
// Store trim information in global state before creating trimmer
|
|
||||||
// globalState.trimmerStates[filePath] = {
|
|
||||||
// trimStart: file.trimStart || 0,
|
|
||||||
// trimEnd: file.trimEnd || 0,
|
|
||||||
// regionStart: file.trimStart || 0,
|
|
||||||
// regionEnd: file.trimEnd || 0,
|
|
||||||
// originalPath: file.originalPath,
|
|
||||||
// };
|
|
||||||
//}
|
|
||||||
|
|
||||||
createAudioTrimmer(filePath, collection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Create audio trimmer for a single file
|
|
||||||
async function createAudioTrimmer(filePath, section) {
|
|
||||||
// Check if trimmer already exists
|
|
||||||
if (globalState.trimmerElements[filePath]) {
|
|
||||||
return globalState.trimmerElements[filePath];
|
|
||||||
}
|
|
||||||
|
|
||||||
const savedTrimInfo = await ipcRenderer.invoke(
|
|
||||||
'get-trim-info',
|
|
||||||
globalState.currentSection,
|
|
||||||
path.basename(filePath),
|
|
||||||
);
|
|
||||||
// Create trimmer container
|
|
||||||
const trimmerContainer = document.createElement('div');
|
|
||||||
trimmerContainer.classList.add('audio-trimmer-item');
|
|
||||||
trimmerContainer.dataset.filepath = filePath;
|
|
||||||
|
|
||||||
// Create header with title and controls
|
|
||||||
const trimmerHeader = document.createElement('div');
|
|
||||||
trimmerHeader.classList.add('audio-trimmer-header');
|
|
||||||
|
|
||||||
// Title container
|
|
||||||
const titleContainer = document.createElement('div');
|
|
||||||
titleContainer.classList.add('audio-trimmer-title-container');
|
|
||||||
|
|
||||||
if (savedTrimInfo.title) {
|
|
||||||
// Title
|
|
||||||
const title = document.createElement('div');
|
|
||||||
title.classList.add('audio-trimmer-title');
|
|
||||||
title.textContent = savedTrimInfo.title;
|
|
||||||
titleContainer.appendChild(title);
|
|
||||||
|
|
||||||
// Filename
|
|
||||||
const fileName = document.createElement('div');
|
|
||||||
fileName.classList.add('audio-trimmer-filename');
|
|
||||||
fileName.textContent = path.basename(filePath);
|
|
||||||
titleContainer.appendChild(fileName);
|
|
||||||
} else {
|
|
||||||
// Title (using filename if no custom title)
|
|
||||||
const title = document.createElement('div');
|
|
||||||
title.classList.add('audio-trimmer-title');
|
|
||||||
title.textContent = path.basename(filePath);
|
|
||||||
titleContainer.appendChild(title);
|
|
||||||
|
|
||||||
// Filename
|
|
||||||
const fileName = document.createElement('div');
|
|
||||||
fileName.classList.add('audio-trimmer-filename');
|
|
||||||
fileName.textContent = 'hidden';
|
|
||||||
fileName.style.opacity = 0;
|
|
||||||
titleContainer.appendChild(fileName);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Controls container
|
|
||||||
const controlsContainer = document.createElement('div');
|
|
||||||
controlsContainer.classList.add('audio-trimmer-controls');
|
|
||||||
|
|
||||||
// Play/Pause and Save buttons
|
|
||||||
const playPauseBtn = document.createElement('button');
|
|
||||||
playPauseBtn.classList.add('play-pause-btn');
|
|
||||||
playPauseBtn.innerHTML = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
||||||
<path d="M8 5v14l11-7z"/>
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const saveTrimButton = document.createElement('button');
|
|
||||||
saveTrimButton.classList.add('save-trim');
|
|
||||||
saveTrimButton.innerHTML = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
||||||
<path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/>
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const deletebutton = document.createElement('button');
|
|
||||||
deletebutton.classList.add('play-pause-btn');
|
|
||||||
deletebutton.innerHTML = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
||||||
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
|
|
||||||
controlsContainer.appendChild(playPauseBtn);
|
|
||||||
controlsContainer.appendChild(saveTrimButton);
|
|
||||||
controlsContainer.appendChild(deletebutton);
|
|
||||||
|
|
||||||
// Assemble header
|
|
||||||
trimmerHeader.appendChild(titleContainer);
|
|
||||||
trimmerHeader.appendChild(controlsContainer);
|
|
||||||
trimmerContainer.appendChild(trimmerHeader);
|
|
||||||
|
|
||||||
// Waveform container
|
|
||||||
const waveformContainer = document.createElement('div');
|
|
||||||
waveformContainer.classList.add('waveform-container');
|
|
||||||
const waveformId = `waveform-${path.basename(
|
|
||||||
filePath,
|
|
||||||
path.extname(filePath),
|
|
||||||
)}`;
|
|
||||||
waveformContainer.innerHTML = `
|
|
||||||
<div id="${waveformId}" class="waveform"></div>
|
|
||||||
`;
|
|
||||||
trimmerContainer.appendChild(waveformContainer);
|
|
||||||
|
|
||||||
// Time displays
|
|
||||||
const timeInfo = document.createElement('div');
|
|
||||||
timeInfo.classList.add('trim-info');
|
|
||||||
timeInfo.innerHTML = `
|
|
||||||
<div class="trim-time">
|
|
||||||
<span>Start: </span>
|
|
||||||
<span class="trim-start-time">0:00</span>
|
|
||||||
</div>
|
|
||||||
<div class="trim-time">
|
|
||||||
<span>End: </span>
|
|
||||||
<span class="trim-end-time">0:00</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
// const zoomContainer = document.createElement('div');
|
|
||||||
// zoomContainer.className = 'zoom-controls';
|
|
||||||
// zoomContainer.innerHTML = `
|
|
||||||
// <button class="zoom-in">+</button>
|
|
||||||
// <button class="zoom-out">-</button>
|
|
||||||
// <input type="range" min="1" max="200" value="100" class="zoom-slider">
|
|
||||||
// `;
|
|
||||||
// timeInfo.appendChild(zoomContainer);
|
|
||||||
|
|
||||||
// const zoomInBtn = zoomContainer.querySelector('.zoom-in');
|
|
||||||
// const zoomOutBtn = zoomContainer.querySelector('.zoom-out');
|
|
||||||
// const zoomSlider = zoomContainer.querySelector('.zoom-slider');
|
|
||||||
|
|
||||||
// // Zoom functionality
|
|
||||||
// const updateZoom = (zoomLevel) => {
|
|
||||||
// // Get the current scroll position and width
|
|
||||||
// const scrollContainer = wavesurfer.container.querySelector('wave');
|
|
||||||
// const currentScroll = scrollContainer.scrollLeft;
|
|
||||||
// const containerWidth = scrollContainer.clientWidth;
|
|
||||||
|
|
||||||
// // Calculate the center point of the current view
|
|
||||||
// //const centerTime = wavesurfer.getCurrentTime();
|
|
||||||
|
|
||||||
// // Apply zoom
|
|
||||||
// wavesurfer.zoom(zoomLevel);
|
|
||||||
|
|
||||||
// // Recalculate scroll to keep the center point in view
|
|
||||||
// const newDuration = wavesurfer.getDuration();
|
|
||||||
// const pixelsPerSecond = wavesurfer.drawer.width / newDuration;
|
|
||||||
// const centerPixel = centerTime * pixelsPerSecond;
|
|
||||||
|
|
||||||
// // Adjust scroll to keep the center point in the same relative position
|
|
||||||
// const newScrollLeft = centerPixel - (containerWidth / 2);
|
|
||||||
// scrollContainer.scrollLeft = Math.max(0, newScrollLeft);
|
|
||||||
// console.log(currentScroll, newScrollLeft);
|
|
||||||
// };
|
|
||||||
|
|
||||||
// zoomInBtn.addEventListener('click', () => {
|
|
||||||
// const currentZoom = parseInt(zoomSlider.value);
|
|
||||||
// zoomSlider.value = Math.min(currentZoom + 20, 200);
|
|
||||||
// updateZoom(zoomSlider.value);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// zoomOutBtn.addEventListener('click', () => {
|
|
||||||
// const currentZoom = parseInt(zoomSlider.value);
|
|
||||||
// zoomSlider.value = Math.max(currentZoom - 20, 1);
|
|
||||||
// updateZoom(zoomSlider.value);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// zoomSlider.addEventListener('input', (e) => {
|
|
||||||
// updateZoom(e.target.value);
|
|
||||||
// });
|
|
||||||
|
|
||||||
trimmerContainer.appendChild(timeInfo);
|
|
||||||
|
|
||||||
// Add to list and global state
|
|
||||||
audioTrimmersList.appendChild(trimmerContainer);
|
|
||||||
globalState.trimmerElements[filePath] = trimmerContainer;
|
|
||||||
|
|
||||||
// Determine the file to load (original or current)
|
|
||||||
const fileToLoad =
|
|
||||||
section === 'untrimmed'
|
|
||||||
? filePath
|
|
||||||
: globalState.trimmerStates[filePath]?.originalPath || filePath;
|
|
||||||
|
|
||||||
// Setup wavesurfer
|
|
||||||
const wavesurfer = WaveSurfer.create({
|
|
||||||
container: `#${waveformId}`,
|
|
||||||
waveColor: '#ccb1ff',
|
|
||||||
progressColor: '#6e44ba',
|
|
||||||
responsive: true,
|
|
||||||
height: 100,
|
|
||||||
hideScrollbar: true,
|
|
||||||
// barWidth: 2,
|
|
||||||
// barRadius: 3,
|
|
||||||
cursorWidth: 1,
|
|
||||||
backend: 'WebAudio',
|
|
||||||
plugins: [
|
|
||||||
RegionsPlugin.create({
|
|
||||||
color: 'rgba(132, 81, 224, 0.3)',
|
|
||||||
drag: false,
|
|
||||||
resize: true,
|
|
||||||
dragSelection: {
|
|
||||||
slop: 20,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
// ZoomPlugin.create({
|
|
||||||
// // the amount of zoom per wheel step, e.g. 0.5 means a 50% magnification per scroll
|
|
||||||
// scale: 0.5,
|
|
||||||
// // Optionally, specify the maximum pixels-per-second factor while zooming
|
|
||||||
// maxZoom: 100,
|
|
||||||
// }),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
// Store wavesurfer instance in global state
|
|
||||||
globalState.wavesurferInstances[filePath] = wavesurfer;
|
|
||||||
|
|
||||||
// Use existing trim state or create new one
|
|
||||||
globalState.trimmerStates[filePath] = globalState.trimmerStates[filePath] ||
|
|
||||||
savedTrimInfo || {
|
|
||||||
trimStart: 0,
|
|
||||||
trimEnd: 0,
|
|
||||||
regionStart: undefined,
|
|
||||||
regionEnd: undefined,
|
|
||||||
originalPath: fileToLoad,
|
|
||||||
};
|
|
||||||
const startTimeDisplay = timeInfo.querySelector('.trim-start-time');
|
|
||||||
const endTimeDisplay = timeInfo.querySelector('.trim-end-time');
|
|
||||||
|
|
||||||
// Load audio file
|
|
||||||
wavesurfer.load(`file://${fileToLoad}`);
|
|
||||||
|
|
||||||
// Setup play/pause button
|
|
||||||
playPauseBtn.onclick = () => {
|
|
||||||
const instanceState = globalState.trimmerStates[filePath];
|
|
||||||
if (wavesurfer.isPlaying()) {
|
|
||||||
wavesurfer.pause();
|
|
||||||
playPauseBtn.innerHTML = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
||||||
<path d="M8 5v14l11-7z"/>
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
} else {
|
|
||||||
// Always start from the trim start
|
|
||||||
wavesurfer.play(instanceState.trimStart, instanceState.trimEnd);
|
|
||||||
playPauseBtn.innerHTML = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
||||||
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// When audio is ready
|
|
||||||
wavesurfer.on('ready', async () => {
|
|
||||||
const instanceState = globalState.trimmerStates[filePath];
|
|
||||||
|
|
||||||
// Set trim times based on saved state or full duration
|
|
||||||
if (instanceState.trimStart) {
|
|
||||||
// Create initial region covering trim or full duration
|
|
||||||
wavesurfer.clearRegions();
|
|
||||||
const region = wavesurfer.addRegion({
|
|
||||||
start: instanceState.trimStart,
|
|
||||||
end: instanceState.trimEnd,
|
|
||||||
color: 'rgba(132, 81, 224, 0.3)',
|
|
||||||
drag: false,
|
|
||||||
resize: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
instanceState.trimStart = instanceState.trimStart || 0;
|
|
||||||
instanceState.trimEnd = instanceState.trimEnd || wavesurfer.getDuration();
|
|
||||||
|
|
||||||
// Update time displays
|
|
||||||
startTimeDisplay.textContent = formatTime(instanceState.trimStart);
|
|
||||||
endTimeDisplay.textContent = formatTime(instanceState.trimEnd);
|
|
||||||
|
|
||||||
// Store region details
|
|
||||||
instanceState.regionStart = instanceState.trimStart;
|
|
||||||
instanceState.regionEnd = instanceState.trimEnd;
|
|
||||||
|
|
||||||
// Listen for region updates
|
|
||||||
wavesurfer.on('region-update-end', async (updatedRegion) => {
|
|
||||||
// Ensure the region doesn't exceed audio duration
|
|
||||||
instanceState.trimStart = Math.max(0, updatedRegion.start);
|
|
||||||
instanceState.trimEnd = Math.min(
|
|
||||||
wavesurfer.getDuration(),
|
|
||||||
updatedRegion.end,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update time displays
|
|
||||||
startTimeDisplay.textContent = formatTime(instanceState.trimStart);
|
|
||||||
endTimeDisplay.textContent = formatTime(instanceState.trimEnd);
|
|
||||||
|
|
||||||
// Store updated region details
|
|
||||||
instanceState.regionStart = instanceState.trimStart;
|
|
||||||
instanceState.regionEnd = instanceState.trimEnd;
|
|
||||||
|
|
||||||
globalState.trimmerStates[filePath] = instanceState;
|
|
||||||
|
|
||||||
// Adjust region if it exceeds bounds
|
|
||||||
updatedRegion.update({
|
|
||||||
start: instanceState.trimStart,
|
|
||||||
end: instanceState.trimEnd,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle region creation
|
|
||||||
wavesurfer.on('region-created', (newRegion) => {
|
|
||||||
// Remove all other regions
|
|
||||||
Object.keys(wavesurfer.regions.list).forEach((id) => {
|
|
||||||
if (wavesurfer.regions.list[id] !== newRegion) {
|
|
||||||
wavesurfer.regions.list[id].remove();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Reset to trim start when audio finishes
|
|
||||||
wavesurfer.on('finish', () => {
|
|
||||||
wavesurfer.setCurrentTime(instanceState.trimStart);
|
|
||||||
playPauseBtn.innerHTML = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
||||||
<path d="M8 5v14l11-7z"/>
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Save trimmed audio functionality
|
|
||||||
saveTrimButton.addEventListener('click', async () => {
|
|
||||||
try {
|
|
||||||
// Get current collections
|
|
||||||
const collections = await ipcRenderer.invoke('get-collections');
|
|
||||||
|
|
||||||
// Create a dialog to select or create a collection
|
|
||||||
const dialogHtml = `
|
|
||||||
<div id="save-collection-dialog"
|
|
||||||
style="
|
|
||||||
position: fixed;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
background-color: #2a2a2a;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
||||||
z-index: 1000;
|
|
||||||
">
|
|
||||||
<div style="">
|
|
||||||
<input type="text" id="new-save-title" placeholder="Title">
|
|
||||||
</div>
|
|
||||||
<select id="existing-collections" style="width: 100%; margin-top: 10px;">
|
|
||||||
${collections
|
|
||||||
.map((col) =>
|
|
||||||
col === 'untrimmed'
|
|
||||||
? ''
|
|
||||||
: `<option value="${col}" ${
|
|
||||||
globalState.currentSection === col ? 'selected' : ''
|
|
||||||
}>${col}</option>`,
|
|
||||||
)
|
|
||||||
.join('')}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<div style="margin-top: 10px; display: flex; justify-content: space-between;">
|
|
||||||
<button class="play-pause-btn" id="cancel-save-btn" style="width: 48%; ">Cancel</button>
|
|
||||||
<button class="play-pause-btn" id="save-to-collection-btn" style="width: 48%;">Save</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Create dialog overlay
|
|
||||||
const overlay = document.createElement('div');
|
|
||||||
overlay.style.position = 'fixed';
|
|
||||||
overlay.style.top = '0';
|
|
||||||
overlay.style.left = '0';
|
|
||||||
overlay.style.width = '100%';
|
|
||||||
overlay.style.height = '100%';
|
|
||||||
overlay.style.backgroundColor = 'rgba(0,0,0,0.5)';
|
|
||||||
overlay.style.zIndex = '999';
|
|
||||||
overlay.innerHTML = dialogHtml;
|
|
||||||
document.body.appendChild(overlay);
|
|
||||||
|
|
||||||
const existingCollectionsSelect = overlay.querySelector(
|
|
||||||
'#existing-collections',
|
|
||||||
);
|
|
||||||
|
|
||||||
const newSaveTitleInput = overlay.querySelector('#new-save-title');
|
|
||||||
const createCollectionBtn = overlay.querySelector(
|
|
||||||
'#create-collection-btn',
|
|
||||||
);
|
|
||||||
const saveToCollectionBtn = overlay.querySelector(
|
|
||||||
'#save-to-collection-btn',
|
|
||||||
);
|
|
||||||
const cancelSaveBtn = overlay.querySelector('#cancel-save-btn');
|
|
||||||
|
|
||||||
if (savedTrimInfo.title) {
|
|
||||||
newSaveTitleInput.value = savedTrimInfo.title;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save to collection
|
|
||||||
saveToCollectionBtn.addEventListener('click', async () => {
|
|
||||||
const newTitle = document
|
|
||||||
.getElementById('new-save-title')
|
|
||||||
.value.trim();
|
|
||||||
const settings = await ipcRenderer.invoke('load-settings');
|
|
||||||
|
|
||||||
const selectedCollection = existingCollectionsSelect.value;
|
|
||||||
|
|
||||||
if (!selectedCollection) {
|
|
||||||
alert('Please select or create a collection');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await ipcRenderer.invoke(
|
|
||||||
'delete-old-file',
|
|
||||||
settings.outputFolder,
|
|
||||||
globalState.currentSection,
|
|
||||||
savedTrimInfo.title,
|
|
||||||
);
|
|
||||||
await ipcRenderer.invoke(
|
|
||||||
'save-trimmed-file',
|
|
||||||
path.basename(filePath),
|
|
||||||
globalState.currentSection,
|
|
||||||
selectedCollection,
|
|
||||||
instanceState.trimStart,
|
|
||||||
instanceState.trimEnd,
|
|
||||||
newTitle,
|
|
||||||
);
|
|
||||||
|
|
||||||
const saveResult = await ipcRenderer.invoke(
|
|
||||||
'save-trimmed-audio',
|
|
||||||
{
|
|
||||||
originalFilePath: filePath,
|
|
||||||
outputFolder: settings.outputFolder,
|
|
||||||
collectionName: selectedCollection,
|
|
||||||
title: newTitle,
|
|
||||||
trimStart: instanceState.trimStart,
|
|
||||||
trimEnd: instanceState.trimEnd,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (saveResult.success) {
|
|
||||||
// Close save dialog
|
|
||||||
// Remove dialog
|
|
||||||
document.body.removeChild(overlay);
|
|
||||||
trimmerContainer.remove();
|
|
||||||
await loadCollectionFiles(globalState.currentSection);
|
|
||||||
await populateCollectionsList();
|
|
||||||
|
|
||||||
// Optional: Show success message
|
|
||||||
//alert(`Trimmed audio saved to ${saveResult.filePath}`);
|
|
||||||
} else {
|
|
||||||
alert(`Failed to save trimmed audio: ${saveResult.error}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh the view
|
|
||||||
} catch (error) {
|
|
||||||
alert('Error saving file: ' + error.message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cancel button
|
|
||||||
cancelSaveBtn.addEventListener('click', () => {
|
|
||||||
document.body.removeChild(overlay);
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error creating save dialog:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
deletebutton.addEventListener('click', async () => {
|
|
||||||
// Create confirmation dialog
|
|
||||||
const confirmDelete = confirm(
|
|
||||||
`Are you sure you want to delete this audio file?\nThis will remove the original file and any trimmed versions.`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (confirmDelete) {
|
|
||||||
try {
|
|
||||||
// Delete original file
|
|
||||||
await ipcRenderer.invoke('delete-file', filePath);
|
|
||||||
|
|
||||||
// Remove from UI
|
|
||||||
trimmerContainer.remove();
|
|
||||||
|
|
||||||
// Optional: Notify user
|
|
||||||
alert('File deleted successfully');
|
|
||||||
|
|
||||||
// Refresh the current section view
|
|
||||||
await loadCollectionFiles(globalState.currentSection);
|
|
||||||
await populateCollectionsList();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting file:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return trimmerContainer;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial load of untrimmed files and collections
|
|
||||||
await loadCollectionFiles('untrimmed');
|
|
||||||
await populateCollectionsList();
|
|
||||||
|
|
||||||
// Listen for new untrimmed files
|
|
||||||
ipcRenderer.on('new-untrimmed-file', async (event, filePath) => {
|
|
||||||
// Refresh the untrimmed section
|
|
||||||
await loadCollectionFiles('untrimmed');
|
|
||||||
await populateCollectionsList();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Periodic refresh
|
|
||||||
setInterval(async () => {
|
|
||||||
await populateCollectionsList();
|
|
||||||
await loadCollectionFiles(globalState.currentSection);
|
|
||||||
}, 5000);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add collection button handler
|
|
||||||
document
|
|
||||||
.getElementById('add-collection-btn')
|
|
||||||
.addEventListener('click', async () => {
|
|
||||||
try {
|
|
||||||
// Create a dialog to input new collection name
|
|
||||||
const dialogHtml = `
|
|
||||||
<div id="new-collection-dialog" style="
|
|
||||||
position: fixed;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
background-color: #2a2a2a;
|
|
||||||
padding: 0px 10px 10px 10px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
||||||
z-index: 1000;
|
|
||||||
">
|
|
||||||
<h4>Create New Collection</h4>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="new-collection-input"
|
|
||||||
placeholder="Enter collection name"
|
|
||||||
style="width: 100%; align-self: center; padding: 10px; margin-bottom: 10px;"
|
|
||||||
>
|
|
||||||
<div style="display: flex; justify-content: space-between;">
|
|
||||||
|
|
||||||
<button id="create-collection-cancel-btn" class="play-pause-btn" style="width: 48%; ">Cancel</button>
|
|
||||||
<button id="create-collection-confirm-btn" class="play-pause-btn" style="width: 48%; ">Create</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Create dialog overlay
|
|
||||||
const overlay = document.createElement('div');
|
|
||||||
overlay.style.position = 'fixed';
|
|
||||||
overlay.style.top = '0';
|
|
||||||
overlay.style.left = '0';
|
|
||||||
overlay.style.width = '100%';
|
|
||||||
overlay.style.height = '100%';
|
|
||||||
overlay.style.backgroundColor = 'rgba(0,0,0,0.5)';
|
|
||||||
overlay.style.zIndex = '999';
|
|
||||||
overlay.innerHTML = dialogHtml;
|
|
||||||
document.body.appendChild(overlay);
|
|
||||||
|
|
||||||
const newCollectionInput = overlay.querySelector('#new-collection-input');
|
|
||||||
const createCollectionConfirmBtn = overlay.querySelector(
|
|
||||||
'#create-collection-confirm-btn',
|
|
||||||
);
|
|
||||||
const createCollectionCancelBtn = overlay.querySelector(
|
|
||||||
'#create-collection-cancel-btn',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create collection when confirm button is clicked
|
|
||||||
createCollectionConfirmBtn.addEventListener('click', async () => {
|
|
||||||
const newCollectionName = newCollectionInput.value.trim();
|
|
||||||
|
|
||||||
if (newCollectionName) {
|
|
||||||
try {
|
|
||||||
await ipcRenderer.invoke('add-new-collection', newCollectionName);
|
|
||||||
|
|
||||||
// Remove dialog
|
|
||||||
document.body.removeChild(overlay);
|
|
||||||
|
|
||||||
// Refresh collections list
|
|
||||||
await populateCollectionsList();
|
|
||||||
} catch (error) {
|
|
||||||
// Show error in the dialog
|
|
||||||
const errorDiv = document.createElement('div');
|
|
||||||
errorDiv.textContent = error.message;
|
|
||||||
errorDiv.style.color = 'red';
|
|
||||||
errorDiv.style.marginTop = '10px';
|
|
||||||
overlay.querySelector('div').appendChild(errorDiv);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Show error if input is empty
|
|
||||||
const errorDiv = document.createElement('div');
|
|
||||||
errorDiv.textContent = 'Collection name cannot be empty';
|
|
||||||
errorDiv.style.color = 'red';
|
|
||||||
errorDiv.style.marginTop = '10px';
|
|
||||||
overlay.querySelector('div').appendChild(errorDiv);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Cancel button closes the dialog
|
|
||||||
createCollectionCancelBtn.addEventListener('click', () => {
|
|
||||||
document.body.removeChild(overlay);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Focus the input when dialog opens
|
|
||||||
newCollectionInput.focus();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error creating new collection dialog:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@ -1,355 +0,0 @@
|
|||||||
body {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background-color: #1e1e1e;
|
|
||||||
height: 100vh;
|
|
||||||
overflow: hidden;
|
|
||||||
color: #ffffffd3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.titlebar {
|
|
||||||
height: 30px;
|
|
||||||
background: #262626;
|
|
||||||
-webkit-app-region: drag;
|
|
||||||
color: white;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border: none;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-container {
|
|
||||||
display: flex;
|
|
||||||
height: calc(100vh);
|
|
||||||
margin-top: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar {
|
|
||||||
width: 250px;
|
|
||||||
background-color: #1e1e1e;
|
|
||||||
border-right: 1px solid #303030;
|
|
||||||
padding: 5px 20px 20px 20px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-section {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-section h3 {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
border-bottom: 1px solid #393939;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-section .add-collection-btn {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
margin-top: 10px;
|
|
||||||
background-color: #6e44ba;
|
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #ffffffd3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sidebar-section .add-collection-btn:hover {
|
|
||||||
background-color: #4f3186;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-item, .collection-item {
|
|
||||||
padding: 10px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background-color 0.2s;
|
|
||||||
margin-bottom: 3px;
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-item:hover, .collection-item:hover {
|
|
||||||
background-color: #303030;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-item.active, .collection-item.active {
|
|
||||||
background-color: rgba(110, 68, 186, 0.3);
|
|
||||||
color: #ccb1ff;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-content {
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audio-trimmers-section {
|
|
||||||
background-color: #1e1e1e;
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audio-trimmers-list {
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
padding: 20px;
|
|
||||||
scrollbar-width: none; /* Firefox */
|
|
||||||
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
|
||||||
}
|
|
||||||
|
|
||||||
.audio-trimmers-list::-webkit-scrollbar { /* WebKit */
|
|
||||||
width: 0;
|
|
||||||
height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audio-trimmer-item {
|
|
||||||
position: relative;
|
|
||||||
background-color: #1e1e1e;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 15px;
|
|
||||||
margin-bottom: 0px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.audio-trimmer-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audio-trimmer-title-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audio-trimmer-title {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 18px;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audio-trimmer-filename {
|
|
||||||
color: #888;
|
|
||||||
font-weight: regular;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audio-trimmer-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.waveform-container {
|
|
||||||
width: 100%;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.waveform {
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.audio-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.trim-info {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
/* margin-bottom: 20px; */
|
|
||||||
}
|
|
||||||
|
|
||||||
.trim-time {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.play-pause-btn, .save-trim {
|
|
||||||
background-color: #6e44ba;
|
|
||||||
border: none;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
color: #ffffffd3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.play-pause-btn:hover, .save-trim:hover {
|
|
||||||
background-color: #4f3186;
|
|
||||||
}
|
|
||||||
|
|
||||||
.play-pause-btn svg, .save-trim svg {
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
fill: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
background-color: #383838;
|
|
||||||
border-radius: 5px;
|
|
||||||
border-color:#303030;
|
|
||||||
color: #ffffffd3;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
input{
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
background-color: #383838;
|
|
||||||
border-radius: 5px;
|
|
||||||
border-color:#303030;
|
|
||||||
color: #ffffffd3;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
input:focus {
|
|
||||||
border-color: #303030;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
select:focus {
|
|
||||||
border-color: #303030;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
select:active {
|
|
||||||
border-color: #303030;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Settings Modal */
|
|
||||||
.modal {
|
|
||||||
display: none;
|
|
||||||
position: fixed;
|
|
||||||
z-index: 1000;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
overflow: auto;
|
|
||||||
background-color: rgba(0,0,0,0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
position: relative;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
background-color: #2a2a2a;
|
|
||||||
margin: auto;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 10px;
|
|
||||||
width: 500px;
|
|
||||||
color: #ffffffd3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-group {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-group label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.close-modal {
|
|
||||||
color: #aaa;
|
|
||||||
float: right;
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: bold;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-modal:hover {
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
#save-settings {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
background-color: #6e44ba;
|
|
||||||
color: #ffffffd3;
|
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
#save-settings:hover {
|
|
||||||
background-color: #4f3186;
|
|
||||||
}
|
|
||||||
|
|
||||||
#select-output-folder {
|
|
||||||
width: 15%;
|
|
||||||
height: 36px;
|
|
||||||
padding: 10px;
|
|
||||||
background-color: #6e44ba;
|
|
||||||
color: #ffffffd3;
|
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
#select-output-folder:hover {
|
|
||||||
background-color: #4f3186;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#input-device {
|
|
||||||
width: 100%;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
#output-folder {
|
|
||||||
width: 84%;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-btn {
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-btn svg {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
fill: #ffffffd3;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Zoom controls styling */
|
|
||||||
.zoom-controls {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
.zoom-controls button {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.zoom-controls .zoom-slider {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#recording-length, #osc-port {
|
|
||||||
width: 20%;
|
|
||||||
}
|
|
||||||
118
electron-ui/src/redux/main.ts
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import { createSlice, configureStore } from '@reduxjs/toolkit';
|
||||||
|
import { ClipMetadata, MetadataState } from './types';
|
||||||
|
|
||||||
|
const initialState: MetadataState = {
|
||||||
|
collections: [],
|
||||||
|
};
|
||||||
|
const metadataSlice = createSlice({
|
||||||
|
name: 'metadata',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setAllData(state, action) {
|
||||||
|
state.collections = action.payload.collections;
|
||||||
|
},
|
||||||
|
setCollections(state, action) {
|
||||||
|
const { collection, newMetadata } = action.payload;
|
||||||
|
const index = state.collections.findIndex(
|
||||||
|
(col) => col.name === collection,
|
||||||
|
);
|
||||||
|
if (index !== -1) {
|
||||||
|
state.collections[index] = newMetadata;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addCollection(state, action) {
|
||||||
|
const name = action.payload;
|
||||||
|
if (!state.collections.find((col) => col.name === name)) {
|
||||||
|
state.collections.push({ name, id: Date.now(), clips: [] });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
editClip(state, action) {
|
||||||
|
const { collection, clip } = action.payload;
|
||||||
|
const collectionState = state.collections.find(
|
||||||
|
(col) => col.name === collection,
|
||||||
|
);
|
||||||
|
// console.log('Editing clip in collection:', collection, clip);
|
||||||
|
if (collectionState) {
|
||||||
|
const index = collectionState.clips.findIndex(
|
||||||
|
(c) => c.filename === clip.filename,
|
||||||
|
);
|
||||||
|
if (index !== -1) {
|
||||||
|
collectionState.clips[index] = clip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteClip(state, action) {
|
||||||
|
const { collection, clip } = action.payload;
|
||||||
|
const collectionState = state.collections.find(
|
||||||
|
(col) => col.name === collection,
|
||||||
|
);
|
||||||
|
if (collectionState) {
|
||||||
|
collectionState.clips = collectionState.clips.filter(
|
||||||
|
(c) => c.filename !== clip.filename,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
moveClip(state, action) {
|
||||||
|
const { sourceCollection, targetCollection, clip } = action.payload;
|
||||||
|
const sourceState = state.collections.find(
|
||||||
|
(col) => col.name === sourceCollection,
|
||||||
|
);
|
||||||
|
const targetState = state.collections.find(
|
||||||
|
(col) => col.name === targetCollection,
|
||||||
|
);
|
||||||
|
if (sourceState && targetState) {
|
||||||
|
sourceState.clips = sourceState.clips.filter(
|
||||||
|
(c) => c.filename !== clip.filename,
|
||||||
|
);
|
||||||
|
targetState.clips.push(clip);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addNewClips(state, action) {
|
||||||
|
const { collections } = action.payload;
|
||||||
|
Object.keys(collections).forEach((collection) => {
|
||||||
|
const collectionState = state.collections.find(
|
||||||
|
(col) => col.name === collection,
|
||||||
|
);
|
||||||
|
if (!collectionState) {
|
||||||
|
state.collections.push({
|
||||||
|
name: collection,
|
||||||
|
id: Date.now(),
|
||||||
|
clips: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const existingFilenames = new Set(
|
||||||
|
state.collections
|
||||||
|
.find((col) => col.name === collection)
|
||||||
|
?.clips.map((clip) => clip.filename) || [],
|
||||||
|
);
|
||||||
|
const newClips = collections[collection].filter(
|
||||||
|
(clip: ClipMetadata) => !existingFilenames.has(clip.filename),
|
||||||
|
);
|
||||||
|
// const collectionState = state.collections.find(
|
||||||
|
// (col) => col.name === collection,
|
||||||
|
// );
|
||||||
|
if (collectionState) {
|
||||||
|
collectionState.clips.push(...newClips);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const store = configureStore({
|
||||||
|
reducer: metadataSlice.reducer,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Can still subscribe to the store
|
||||||
|
// store.subscribe(() => console.log(store.getState()));
|
||||||
|
|
||||||
|
// Get the type of our store variable
|
||||||
|
export type AppStore = typeof store;
|
||||||
|
// Infer the `RootState` and `AppDispatch` types from the store itself
|
||||||
|
export type RootState = ReturnType<AppStore['getState']>;
|
||||||
|
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
|
||||||
|
export type AppDispatch = AppStore['dispatch'];
|
||||||
|
|
||||||
|
export const { setCollections, addNewClips, addCollection } =
|
||||||
|
metadataSlice.actions;
|
||||||
|
export default metadataSlice.reducer;
|
||||||
23
electron-ui/src/redux/types.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
export enum PlaybackType {
|
||||||
|
PlayStop = 'playStop',
|
||||||
|
PlayOverlap = 'playOverlap',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClipMetadata {
|
||||||
|
name: string;
|
||||||
|
filename: string;
|
||||||
|
volume: number;
|
||||||
|
startTime: number | undefined;
|
||||||
|
endTime: number | undefined;
|
||||||
|
playbackType: PlaybackType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CollectionState {
|
||||||
|
name: string;
|
||||||
|
id: number;
|
||||||
|
clips: ClipMetadata[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetadataState {
|
||||||
|
collections: CollectionState[];
|
||||||
|
}
|
||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@ -10,19 +9,25 @@
|
|||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--color-midnight: #1E1E1E;
|
--color-midnight: #1E1E1E;
|
||||||
--color-plum: #4f3186;
|
--color-plum: #6e44ba;
|
||||||
|
--color-plumDark: #4f3186;
|
||||||
--color-offwhite: #d4d4d4;
|
--color-offwhite: #d4d4d4;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
::-webkit-scrollbar {
|
||||||
background-color: #4f3186;
|
height: 12px;
|
||||||
padding: 10px 20px;
|
width: 12px;
|
||||||
border-radius: 10px;
|
background: #1e1e1e;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
::-webkit-scrollbar-thumb {
|
||||||
transform: scale(1.05);
|
background: #303030;
|
||||||
opacity: 1;
|
-webkit-border-radius: 1ex;
|
||||||
|
-webkit-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-corner {
|
||||||
|
background: #1e1e1e;
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
li {
|
||||||
|
|||||||
@ -1,48 +1,149 @@
|
|||||||
import { MemoryRouter as Router, Routes, Route } from 'react-router-dom';
|
import { MemoryRouter as Router, Routes, Route } from 'react-router-dom';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
// import 'tailwindcss/tailwind.css';
|
// import 'tailwindcss/tailwind.css';
|
||||||
import icon from '../../assets/icon.svg';
|
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import AudioTrimmer from './components/AudioTrimer';
|
import ClipList from './components/ClipList';
|
||||||
|
import { useAppDispatch, useAppSelector } from './hooks';
|
||||||
|
import { store } from '../redux/main';
|
||||||
|
|
||||||
|
function MainPage() {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const collections = useAppSelector((state) =>
|
||||||
|
state.collections.map((col) => col.name),
|
||||||
|
);
|
||||||
|
const [selectedCollection, setSelectedCollection] = useState<string>(
|
||||||
|
collections[0] || 'Uncategorized',
|
||||||
|
);
|
||||||
|
const [newCollectionOpen, setNewCollectionOpen] = useState(false);
|
||||||
|
const [newCollectionName, setNewCollectionName] = useState<string>('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchMetadata = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost:5010/meta');
|
||||||
|
const data = await response.json();
|
||||||
|
dispatch({ type: 'metadata/setAllData', payload: data });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching metadata:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchMetadata();
|
||||||
|
const intervalId = setInterval(fetchMetadata, 5000);
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Update selected collection if collections change
|
||||||
|
if (collections.length > 0 && !collections.includes(selectedCollection)) {
|
||||||
|
setSelectedCollection(collections[0]);
|
||||||
|
}
|
||||||
|
}, [collections, selectedCollection]);
|
||||||
|
|
||||||
|
const handleNewCollectionSave = () => {
|
||||||
|
if (
|
||||||
|
newCollectionName.trim() &&
|
||||||
|
!collections.includes(newCollectionName.trim())
|
||||||
|
) {
|
||||||
|
dispatch({
|
||||||
|
type: 'metadata/addCollection',
|
||||||
|
payload: newCollectionName.trim(),
|
||||||
|
});
|
||||||
|
setSelectedCollection(newCollectionName.trim());
|
||||||
|
fetch('http://localhost:5010/meta/collections/add', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name: newCollectionName.trim() }),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.catch((err) => console.error('Error creating collection:', err));
|
||||||
|
}
|
||||||
|
setNewCollectionOpen(false);
|
||||||
|
setNewCollectionName('');
|
||||||
|
};
|
||||||
|
|
||||||
function Hello() {
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen min-w-screen flex flex-col items-center justify-center bg-midnight text-offwhite">
|
<div className="min-h-screen min-w-screen bg-midnight text-offwhite relative">
|
||||||
{/* <div className="Hello">
|
{/* Left Nav Bar - sticky */}
|
||||||
<img width="200" alt="icon" src={icon} />
|
<Dialog
|
||||||
</div>
|
open={newCollectionOpen}
|
||||||
<h1>electron-react-boilerplate</h1>
|
onClose={() => setNewCollectionOpen(false)}
|
||||||
<div className="Hello">
|
slotProps={{
|
||||||
<a
|
paper: { sx: { backgroundColor: '#1a1a1a', color: 'white' } },
|
||||||
href="https://electron-react-boilerplate.js.org/"
|
}}
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
>
|
||||||
<button type="button">
|
<DialogTitle>Edit Clip Name</DialogTitle>
|
||||||
<span role="img" aria-label="books">
|
<DialogContent>
|
||||||
📚
|
<input
|
||||||
</span>
|
autoFocus
|
||||||
Read our docs
|
className="font-bold text-lg bg-transparent outline-none border-b border-plum focus:border-plumDark text-white mb-1 w-full text-center"
|
||||||
</button>
|
type="text"
|
||||||
</a>
|
value={newCollectionName}
|
||||||
<a
|
onChange={(e) => setNewCollectionName(e.target.value)}
|
||||||
href="https://github.com/sponsors/electron-react-boilerplate"
|
onKeyDown={(e) => {
|
||||||
target="_blank"
|
if (e.key === 'Enter') handleNewCollectionSave();
|
||||||
rel="noreferrer"
|
}}
|
||||||
>
|
aria-label="New collection name"
|
||||||
<button type="button">
|
|
||||||
<span role="img" aria-label="folded hands">
|
|
||||||
🙏
|
|
||||||
</span>
|
|
||||||
Donate
|
|
||||||
</button>
|
|
||||||
</a>
|
|
||||||
</div> */}
|
|
||||||
<div className="bg-midnight min-w-screen">
|
|
||||||
<AudioTrimmer
|
|
||||||
title="audio_capture_20251206_123108.wav"
|
|
||||||
filePath="C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250118_000351.wav"
|
|
||||||
// section="Section 1"
|
|
||||||
/>
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setNewCollectionOpen(false);
|
||||||
|
setNewCollectionName('');
|
||||||
|
}}
|
||||||
|
className="bg-plum hover:bg-plumDark text-white font-bold h-10 px-4 rounded-md"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleNewCollectionSave}
|
||||||
|
className="bg-plum hover:bg-plumDark text-white font-bold h-10 px-4 rounded-md"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
<nav
|
||||||
|
className="w-48 h-screen sticky top-0 left-0 border-r border-neutral-700 bg-midnight flex flex-col p-2"
|
||||||
|
style={{ position: 'absolute', top: 0, left: 0 }}
|
||||||
|
>
|
||||||
|
<div className="p-4 font-bold text-lg">Collections</div>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="w-full rounded text-left px-4 py-2 mb-2 bg-plumDark text-offwhite font-semibold hover:bg-plum"
|
||||||
|
onClick={() => setNewCollectionOpen(true)}
|
||||||
|
>
|
||||||
|
+ Create Collection
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<ul className="flex-1 overflow-y-auto">
|
||||||
|
{collections.map((col) => (
|
||||||
|
<li key={col}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`w-full rounded text-left px-4 py-2 mt-2 hover:bg-plumDark ${selectedCollection === col ? 'bg-plum text-offwhite font-semibold' : 'text-offwhite'}`}
|
||||||
|
onClick={() => setSelectedCollection(col)}
|
||||||
|
>
|
||||||
|
{col}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{/* Main Content */}
|
||||||
|
<div
|
||||||
|
className="absolute top-0 ml-[12rem] w-[calc(100%-12rem)] h-screen overflow-y-auto p-4"
|
||||||
|
// style={{ left: '12rem', width: 'calc(100% - 12rem)' }}
|
||||||
|
>
|
||||||
|
<ClipList collection={selectedCollection} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -50,10 +151,12 @@ function Hello() {
|
|||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
|
<Provider store={store}>
|
||||||
<Router>
|
<Router>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Hello />} />
|
<Route path="/" element={<MainPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
|
</Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,82 +5,208 @@ import React, {
|
|||||||
useCallback,
|
useCallback,
|
||||||
useRef,
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import Regions from 'wavesurfer.js/dist/plugins/regions.esm.js';
|
import Dialog from '@mui/material/Dialog';
|
||||||
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
|
import DialogContent from '@mui/material/DialogContent';
|
||||||
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
import { useWavesurfer } from '@wavesurfer/react';
|
import { useWavesurfer } from '@wavesurfer/react';
|
||||||
|
import RegionsPlugin from 'wavesurfer.js/dist/plugins/regions.esm.js';
|
||||||
|
import ZoomPlugin from 'wavesurfer.js/dist/plugins/zoom.esm.js';
|
||||||
|
import PlayArrowIcon from '@mui/icons-material/PlayArrow';
|
||||||
|
import PauseIcon from '@mui/icons-material/Pause';
|
||||||
|
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||||
|
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
import { ClipMetadata } from '../../redux/types';
|
||||||
|
import { useAppSelector } from '../hooks';
|
||||||
|
|
||||||
export interface AudioTrimmerProps {
|
export interface AudioTrimmerProps {
|
||||||
filePath: string;
|
metadata: ClipMetadata;
|
||||||
section: string;
|
onSave?: (metadata: ClipMetadata) => void;
|
||||||
title?: string;
|
onDelete?: (metadata: ClipMetadata) => void;
|
||||||
trimStart?: number;
|
onMove?: (newCollection: string, metadata: ClipMetadata) => void;
|
||||||
trimEnd?: number;
|
|
||||||
onSave?: (trimStart: number, trimEnd: number, title?: string) => void;
|
|
||||||
onDelete?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AudioTrimmer({
|
export default function AudioTrimmer({
|
||||||
filePath,
|
metadata,
|
||||||
section,
|
|
||||||
title,
|
|
||||||
trimStart,
|
|
||||||
trimEnd,
|
|
||||||
onSave,
|
onSave,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onMove,
|
||||||
}: AudioTrimmerProps) {
|
}: AudioTrimmerProps) {
|
||||||
|
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||||
|
useSortable({ id: metadata.filename });
|
||||||
|
|
||||||
|
// Dialog state for editing name
|
||||||
|
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||||
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
|
const [nameInput, setNameInput] = useState<string>(metadata.name);
|
||||||
|
const collectionNames = useAppSelector((state) =>
|
||||||
|
state.collections.map((col) => col.name),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setNameInput(metadata.name);
|
||||||
|
}, [metadata.name]);
|
||||||
|
|
||||||
|
const openEditDialog = () => setEditDialogOpen(true);
|
||||||
|
const closeEditDialog = () => setEditDialogOpen(false);
|
||||||
|
|
||||||
|
const handleDialogSave = () => {
|
||||||
|
if (nameInput.trim() && nameInput !== metadata.name) {
|
||||||
|
const updated = { ...metadata, name: nameInput.trim() };
|
||||||
|
if (onSave) onSave(updated);
|
||||||
|
}
|
||||||
|
closeEditDialog();
|
||||||
|
};
|
||||||
|
|
||||||
const [blobUrl, setBlobUrl] = useState<string | undefined>(undefined);
|
const [blobUrl, setBlobUrl] = useState<string | undefined>(undefined);
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
|
// const [clipStart, setClipStart] = useState<number | undefined>(undefined);
|
||||||
|
// const [clipEnd, setClipEnd] = useState<number | undefined>(undefined);
|
||||||
|
|
||||||
const plugins = useMemo(() => [Regions.create()], []);
|
const plugins = useMemo(
|
||||||
|
() => [
|
||||||
|
RegionsPlugin.create(),
|
||||||
|
ZoomPlugin.create({
|
||||||
|
scale: 0.25,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const fileBaseName =
|
||||||
|
metadata.filename.split('\\').pop()?.split('/').pop() || 'Unknown';
|
||||||
|
|
||||||
const { wavesurfer, isReady, isPlaying } = useWavesurfer({
|
const { wavesurfer, isReady, isPlaying } = useWavesurfer({
|
||||||
container: containerRef,
|
container: containerRef,
|
||||||
height: 100,
|
height: 100,
|
||||||
waveColor: '#ccb1ff',
|
waveColor: '#ccb1ff',
|
||||||
progressColor: '#6e44ba',
|
progressColor: '#6e44ba',
|
||||||
|
hideScrollbar: true,
|
||||||
url: blobUrl,
|
url: blobUrl,
|
||||||
plugins,
|
plugins,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add this ref to always have the latest metadata
|
||||||
|
const metadataRef = useRef(metadata);
|
||||||
|
useEffect(() => {
|
||||||
|
metadataRef.current = metadata;
|
||||||
|
}, [metadata]);
|
||||||
|
|
||||||
const onRegionCreated = useCallback(
|
const onRegionCreated = useCallback(
|
||||||
(newRegion: any) => {
|
(newRegion: any) => {
|
||||||
if (wavesurfer === null) return;
|
if (wavesurfer === null) return;
|
||||||
const allRegions = plugins[0].getRegions();
|
|
||||||
|
const allRegions = (plugins[0] as RegionsPlugin).getRegions();
|
||||||
|
let isNew = metadataRef.current.startTime === undefined;
|
||||||
|
|
||||||
allRegions.forEach((region) => {
|
allRegions.forEach((region) => {
|
||||||
if (region.id !== newRegion.id) {
|
if (region.id !== newRegion.id) {
|
||||||
|
if (
|
||||||
|
region.start === newRegion.start &&
|
||||||
|
region.end === newRegion.end
|
||||||
|
) {
|
||||||
|
newRegion.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
region.remove();
|
region.remove();
|
||||||
|
isNew = !(region.start === 0 && region.end === 0);
|
||||||
|
// console.log('Region replace:', newRegion, region);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isNew) {
|
||||||
|
console.log('Region created:', metadataRef.current);
|
||||||
|
const updated = {
|
||||||
|
...metadataRef.current,
|
||||||
|
startTime: newRegion.start,
|
||||||
|
endTime: newRegion.end,
|
||||||
|
};
|
||||||
|
if (onSave) {
|
||||||
|
onSave(updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[plugins, wavesurfer],
|
[plugins, wavesurfer, onSave],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onRegionUpdated = useCallback(
|
||||||
|
(newRegion: any) => {
|
||||||
|
if (wavesurfer === null) return;
|
||||||
|
|
||||||
|
const updated = {
|
||||||
|
...metadataRef.current,
|
||||||
|
startTime: newRegion.start,
|
||||||
|
endTime: newRegion.end,
|
||||||
|
};
|
||||||
|
if (onSave) {
|
||||||
|
onSave(updated);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[onSave, wavesurfer],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('ready, setting up regions plugin', wavesurfer);
|
const plugin = plugins[0] as RegionsPlugin;
|
||||||
if (trimStart !== undefined && trimEnd !== undefined) {
|
|
||||||
plugins[0].addRegion({
|
if (!isReady) return;
|
||||||
start: trimStart,
|
// console.log('ready, setting up regions plugin', plugin, isReady);
|
||||||
end: trimEnd,
|
if (
|
||||||
|
metadataRef.current.startTime !== undefined &&
|
||||||
|
metadataRef.current.endTime !== undefined
|
||||||
|
) {
|
||||||
|
// setClipStart(metadata.startTime);
|
||||||
|
// setClipEnd(metadata.endTime);
|
||||||
|
// console.log('Adding region from metadata:', metadata);=
|
||||||
|
|
||||||
|
const allRegions = plugin.getRegions();
|
||||||
|
// console.log('Existing regions:', allRegions);
|
||||||
|
if (
|
||||||
|
allRegions.length === 0 ||
|
||||||
|
(allRegions.length === 1 &&
|
||||||
|
allRegions[0].start === 0 &&
|
||||||
|
allRegions[0].end === 0)
|
||||||
|
) {
|
||||||
|
// console.log('adding region from metadata:', metadataRef.current);
|
||||||
|
plugin.addRegion({
|
||||||
|
start: metadataRef.current.startTime,
|
||||||
|
end: metadataRef.current.endTime,
|
||||||
color: 'rgba(132, 81, 224, 0.3)',
|
color: 'rgba(132, 81, 224, 0.3)',
|
||||||
drag: false,
|
drag: false,
|
||||||
resize: true,
|
resize: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// setClipStart(0);
|
||||||
|
// setClipEnd(wavesurfer ? wavesurfer.getDuration() : 0);
|
||||||
|
}
|
||||||
|
}, [isReady, plugins]);
|
||||||
|
|
||||||
plugins[0].enableDragSelection({
|
useEffect(() => {
|
||||||
|
const plugin = plugins[0] as RegionsPlugin;
|
||||||
|
plugin.unAll();
|
||||||
|
plugin.on('region-created', onRegionCreated);
|
||||||
|
plugin.on('region-updated', onRegionUpdated);
|
||||||
|
plugin.enableDragSelection({
|
||||||
color: 'rgba(132, 81, 224, 0.3)',
|
color: 'rgba(132, 81, 224, 0.3)',
|
||||||
});
|
});
|
||||||
plugins[0].on('region-created', onRegionCreated);
|
}, [onRegionCreated, onRegionUpdated, plugins]);
|
||||||
}, [isReady, plugins, wavesurfer, onRegionCreated, trimStart, trimEnd]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let url: string | null = null;
|
let url: string | null = null;
|
||||||
async function fetchAudio() {
|
async function fetchAudio() {
|
||||||
// console.log('Loading audio buffer for file:', filePath);
|
// console.log('Loading audio buffer for file:', filename);
|
||||||
const buffer =
|
const buffer = await window.audio.loadAudioBuffer(metadata.filename);
|
||||||
await window.electron.ipcRenderer.loadAudioBuffer(filePath);
|
// console.log('Received buffer:', buffer.buffer);
|
||||||
if (buffer && !buffer.error) {
|
if (buffer.buffer && !buffer.error) {
|
||||||
const audioData = buffer.data ? new Uint8Array(buffer.data) : buffer;
|
const audioData = buffer.buffer
|
||||||
|
? new Uint8Array(buffer.buffer)
|
||||||
|
: buffer;
|
||||||
url = URL.createObjectURL(new Blob([audioData]));
|
url = URL.createObjectURL(new Blob([audioData]));
|
||||||
|
// console.log('Created blob URL:', url);
|
||||||
setBlobUrl(url);
|
setBlobUrl(url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -88,14 +214,14 @@ export default function AudioTrimmer({
|
|||||||
return () => {
|
return () => {
|
||||||
if (url) URL.revokeObjectURL(url);
|
if (url) URL.revokeObjectURL(url);
|
||||||
};
|
};
|
||||||
}, [filePath]);
|
}, [metadata.filename]);
|
||||||
|
|
||||||
const onPlayPause = () => {
|
const onPlayPause = () => {
|
||||||
if (wavesurfer === null) return;
|
if (wavesurfer === null) return;
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
wavesurfer.pause();
|
wavesurfer.pause();
|
||||||
} else {
|
} else {
|
||||||
const allRegions = plugins[0].getRegions();
|
const allRegions = (plugins[0] as RegionsPlugin).getRegions();
|
||||||
if (allRegions.length > 0) {
|
if (allRegions.length > 0) {
|
||||||
wavesurfer.play(allRegions[0].start, allRegions[0].end);
|
wavesurfer.play(allRegions[0].start, allRegions[0].end);
|
||||||
} else {
|
} else {
|
||||||
@ -104,17 +230,185 @@ export default function AudioTrimmer({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const formatTime = (seconds: number) => {
|
||||||
<div className="shadow-[0_4px_6px_rgba(0,0,0,0.5)] m-2 p-4 rounded-lg bg-darkDrop">
|
const minutes = Math.floor(seconds / 60);
|
||||||
<div>
|
const secs = (seconds % 60).toFixed(0);
|
||||||
<text className="m-2 font-bold text-lg">{title}</text>
|
return `${minutes}:${secs.padStart(2, '0')}`;
|
||||||
</div>
|
};
|
||||||
|
|
||||||
<div className="w-[100%] m-2 ">
|
return (
|
||||||
<div ref={containerRef} />
|
<div
|
||||||
<button type="button" onClick={onPlayPause}>
|
ref={setNodeRef}
|
||||||
{isPlaying ? 'Pause' : 'Play'}
|
style={{
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
position: 'relative',
|
||||||
|
alignItems: 'stretch',
|
||||||
|
}}
|
||||||
|
className="shadow-[0_2px_8px_rgba(0,0,0,0.5)] m-2 rounded-lg bg-darkDrop"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
{...attributes}
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
{...listeners}
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
width: '10px',
|
||||||
|
borderRadius: '5px 0 0 5px',
|
||||||
|
cursor: 'grab',
|
||||||
|
}}
|
||||||
|
className="bg-neutral-800"
|
||||||
|
/>
|
||||||
|
{/* <div className="flex flex-col"> */}
|
||||||
|
<div className="ml-4 mr-2 p-2">
|
||||||
|
<div className="grid justify-items-stretch grid-cols-2">
|
||||||
|
<div className="mb-5px flex flex-col">
|
||||||
|
<span
|
||||||
|
className="font-bold text-lg text-white mb-1 cursor-pointer"
|
||||||
|
onClick={openEditDialog}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
openEditDialog();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="Click to edit name"
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
style={{ outline: 'none' }}
|
||||||
|
>
|
||||||
|
{metadata.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-neutral-500">{fileBaseName}</span>
|
||||||
|
</div>
|
||||||
|
<Dialog
|
||||||
|
open={editDialogOpen}
|
||||||
|
onClose={closeEditDialog}
|
||||||
|
slotProps={{
|
||||||
|
paper: { sx: { backgroundColor: '#1a1a1a', color: 'white' } },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle>Edit Clip Name</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<textarea
|
||||||
|
autoFocus
|
||||||
|
className="font-bold text-lg bg-transparent outline-none border-b border-plum focus:border-plumDark text-white mb-1 w-full text-center resize-y"
|
||||||
|
value={nameInput}
|
||||||
|
onChange={(e) => setNameInput(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
onFocus={(event) => event.target.select()}
|
||||||
|
aria-label="Edit clip name"
|
||||||
|
style={{ minHeight: '3em' }}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={closeEditDialog}
|
||||||
|
className="bg-plum hover:bg-plumDark text-white font-bold h-10 px-4 rounded-md"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleDialogSave}
|
||||||
|
className="bg-plum hover:bg-plumDark text-white font-bold h-10 px-4 rounded-md"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={deleteDialogOpen}
|
||||||
|
onClose={() => setDeleteDialogOpen(false)}
|
||||||
|
slotProps={{
|
||||||
|
paper: { sx: { backgroundColor: '#1a1a1a', color: 'white' } },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle>Confirm Delete</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
Are you sure you want to delete this clip?
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setDeleteDialogOpen(false)}
|
||||||
|
className="bg-plum hover:bg-plumDark text-white font-bold h-10 px-4 rounded-md"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setDeleteDialogOpen(false);
|
||||||
|
if (onDelete) onDelete(metadataRef.current);
|
||||||
|
}}
|
||||||
|
className="bg-plum hover:bg-plumDark text-white font-bold h-10 px-4 rounded-md"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="bg-plum hover:bg-plumDark text-white font-bold h-11 w-11 rounded-md ml-1"
|
||||||
|
onClick={onPlayPause}
|
||||||
|
>
|
||||||
|
{isPlaying ? <PauseIcon /> : <PlayArrowIcon />}
|
||||||
|
</button>
|
||||||
|
<div className="relative inline-block">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="bg-plum hover:bg-plumDark text-white font-bold h-11 w-11 rounded-md ml-1"
|
||||||
|
onClick={() => setDropdownOpen((prev) => !prev)}
|
||||||
|
>
|
||||||
|
{dropdownOpen ? <ArrowDownwardIcon /> : <ArrowForwardIcon />}
|
||||||
|
</button>
|
||||||
|
{dropdownOpen && (
|
||||||
|
<div className="absolute z-10 mt-2 w-40 bg-midnight rounded-md shadow-lg">
|
||||||
|
{collectionNames.map((name) => (
|
||||||
|
<button
|
||||||
|
key={name}
|
||||||
|
type="button"
|
||||||
|
className="block w-full text-left px-4 py-2 text-white hover:bg-plumDark"
|
||||||
|
onClick={() => {
|
||||||
|
setDropdownOpen(false);
|
||||||
|
if (onMove) onMove(name, metadata);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="bg-plum hover:bg-plumDark text-white font-bold h-11 w-11 rounded-md ml-1"
|
||||||
|
onClick={() => setDeleteDialogOpen(true)}
|
||||||
|
>
|
||||||
|
<DeleteIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="m-1 wavesurfer-scroll-container">
|
||||||
|
<div ref={containerRef} className="wavesurfer-inner" />
|
||||||
|
</div>
|
||||||
|
<div className="grid justify-items-stretch grid-cols-2 text-neutral-500">
|
||||||
|
<div className="m-1 flex justify-start">
|
||||||
|
<text className="text-sm ">
|
||||||
|
Clip: {formatTime(metadata.startTime ?? 0)} -{' '}
|
||||||
|
{formatTime(metadata.endTime ?? 0)}
|
||||||
|
</text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
205
electron-ui/src/renderer/components/ClipList.tsx
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
DndContext,
|
||||||
|
closestCenter,
|
||||||
|
PointerSensor,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
} from '@dnd-kit/core';
|
||||||
|
import {
|
||||||
|
arrayMove,
|
||||||
|
SortableContext,
|
||||||
|
verticalListSortingStrategy,
|
||||||
|
} from '@dnd-kit/sortable';
|
||||||
|
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
|
||||||
|
import AudioTrimmer from './AudioTrimer';
|
||||||
|
import { ClipMetadata } from '../../redux/types';
|
||||||
|
import { useAppDispatch, useAppSelector } from '../hooks';
|
||||||
|
|
||||||
|
export interface ClipListProps {
|
||||||
|
collection: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ClipList({ collection }: ClipListProps) {
|
||||||
|
const metadata = useAppSelector(
|
||||||
|
(state) =>
|
||||||
|
state.collections.find((col) => col.name === collection) || { clips: [] },
|
||||||
|
);
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
|
||||||
|
);
|
||||||
|
|
||||||
|
async function handleDragEnd(event: any) {
|
||||||
|
const { active, over } = event;
|
||||||
|
if (active.id !== over?.id) {
|
||||||
|
const oldIndex = metadata.clips.findIndex(
|
||||||
|
(item) => item.filename === active.id,
|
||||||
|
);
|
||||||
|
const newIndex = metadata.clips.findIndex(
|
||||||
|
(item) => item.filename === over.id,
|
||||||
|
);
|
||||||
|
const newMetadata = {
|
||||||
|
...metadata,
|
||||||
|
clips: arrayMove(metadata.clips, oldIndex, newIndex),
|
||||||
|
};
|
||||||
|
console.log('New order:', newMetadata);
|
||||||
|
dispatch({
|
||||||
|
type: 'metadata/setCollections',
|
||||||
|
payload: { collection, newMetadata },
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
'http://localhost:5010/meta/collection/clips/reorder',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: collection,
|
||||||
|
clips: newMetadata.clips,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('handle reorder return:', data.collections);
|
||||||
|
dispatch({ type: 'metadata/setAllData', payload: data });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving new clip order:', error);
|
||||||
|
}
|
||||||
|
// setMetadata(newMetadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDelete(meta: ClipMetadata) {
|
||||||
|
dispatch({
|
||||||
|
type: 'metadata/deleteClip',
|
||||||
|
payload: { collection, clip: meta },
|
||||||
|
});
|
||||||
|
fetch('http://localhost:5010/meta/collection/clips/remove', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: collection,
|
||||||
|
clip: meta,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.catch((err) => console.error('Error deleting clip:', err));
|
||||||
|
console.log('Deleting clip:', meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleClipMove(targetCollection: string, meta: ClipMetadata) {
|
||||||
|
console.log('Moving clip:', meta, 'to collection:', targetCollection);
|
||||||
|
dispatch({
|
||||||
|
type: 'metadata/moveClip',
|
||||||
|
payload: { sourceCollection: collection, targetCollection, clip: meta },
|
||||||
|
});
|
||||||
|
fetch('http://localhost:5010/meta/collection/clips/move', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
sourceCollection: collection,
|
||||||
|
targetCollection,
|
||||||
|
clip: meta,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.catch((err) => console.error('Error moving clip:', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleClipSave(meta: ClipMetadata) {
|
||||||
|
try {
|
||||||
|
dispatch({
|
||||||
|
type: 'metadata/editClip',
|
||||||
|
payload: { collection, clip: meta },
|
||||||
|
});
|
||||||
|
const response = await fetch(
|
||||||
|
'http://localhost:5010/meta/collection/clips/edit',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
name: collection,
|
||||||
|
clip: meta,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await response.json();
|
||||||
|
// console.log('handle clip save return:', data.collections);
|
||||||
|
dispatch({
|
||||||
|
type: 'metadata/editClip',
|
||||||
|
payload: { collection, clip: meta },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving clip metadata:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-full flex flex-col justify-start bg-midnight text-offwhite">
|
||||||
|
<DndContext
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
modifiers={[restrictToVerticalAxis]}
|
||||||
|
>
|
||||||
|
<SortableContext
|
||||||
|
items={metadata.clips.map((item) => item.filename)}
|
||||||
|
strategy={verticalListSortingStrategy}
|
||||||
|
>
|
||||||
|
{metadata.clips.map((trimmer, idx) => (
|
||||||
|
<React.Fragment key={trimmer.filename}>
|
||||||
|
<AudioTrimmer
|
||||||
|
metadata={trimmer}
|
||||||
|
onSave={handleClipSave}
|
||||||
|
onDelete={handleDelete}
|
||||||
|
onMove={handleClipMove}
|
||||||
|
/>
|
||||||
|
{(idx + 1) % 10 === 0 && idx !== metadata.clips.length - 1 && (
|
||||||
|
<div className="my-4 border-t border-gray-500">
|
||||||
|
<p className="text-center text-sm text-gray-400">
|
||||||
|
-- Page {Math.ceil((idx + 1) / 10) + 1} --
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
{/* {metadata.map((trimmer) => (
|
||||||
|
<AudioTrimmer
|
||||||
|
key={trimmer.filename}
|
||||||
|
filename={trimmer.filename}
|
||||||
|
onSave={handleClipSave}
|
||||||
|
/>
|
||||||
|
))} */}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
</div>
|
||||||
|
// <div className="min-h-screen flex flex-col justify-center bg-midnight text-offwhite">
|
||||||
|
// <AudioTrimmer
|
||||||
|
// title="audio_capture_20251206_123108.wav"
|
||||||
|
// filePath="C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250118_000351.wav"
|
||||||
|
// // section="Section 1"
|
||||||
|
// />
|
||||||
|
// <AudioTrimmer
|
||||||
|
// title="audio_capture_20251206_123108.wav"
|
||||||
|
// filePath="C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250118_000351.wav"
|
||||||
|
// // section="Section 1"
|
||||||
|
// />
|
||||||
|
// <AudioTrimmer
|
||||||
|
// title="audio_capture_20251206_123108.wav"
|
||||||
|
// filePath="C:\\Users\\mickl\\Music\\clips\\original\\audio_capture_20250118_000351.wav"
|
||||||
|
// // section="Section 1"
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
);
|
||||||
|
}
|
||||||
6
electron-ui/src/renderer/hooks.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { useDispatch, useSelector, useStore } from 'react-redux';
|
||||||
|
import type { AppDispatch, RootState } from '../redux/main';
|
||||||
|
|
||||||
|
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
|
||||||
|
export const useAppSelector = useSelector.withTypes<RootState>();
|
||||||
|
export const useAppStore = useStore.withTypes<RootState>();
|
||||||
3
electron-ui/src/renderer/preload.d.ts
vendored
@ -1,9 +1,10 @@
|
|||||||
import { ElectronHandler } from '../main/preload';
|
import { ElectronHandler, FileHandler } from '../main/preload';
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
interface Window {
|
interface Window {
|
||||||
electron: ElectronHandler;
|
electron: ElectronHandler;
|
||||||
|
audio: FileHandler;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
7
stream_deck_plugin/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
packages/
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
ClipTrimDotNet/bin/
|
||||||
|
ClipTrimDotNet/obj/
|
||||||
|
ClipTrimDotNet/dist/
|
||||||
|
ClipTrimDotNet/node_modules/
|
||||||
1026
stream_deck_plugin/.vs/ClipTrimDotNet/config/applicationhost.config
Normal file
BIN
stream_deck_plugin/.vs/ClipTrimDotNet/v17/.suo
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
{
|
||||||
|
"Version": 1,
|
||||||
|
"WorkspaceRootPath": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\",
|
||||||
|
"Documents": [
|
||||||
|
{
|
||||||
|
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\client\\cliptrimclient.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||||
|
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\client\\cliptrimclient.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\player.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||||
|
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\player.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\profileswitcher.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||||
|
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\profileswitcher.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\client\\collectionmetadata.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||||
|
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\client\\collectionmetadata.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"DocumentGroupContainers": [
|
||||||
|
{
|
||||||
|
"Orientation": 0,
|
||||||
|
"VerticalTabListWidth": 256,
|
||||||
|
"DocumentGroups": [
|
||||||
|
{
|
||||||
|
"DockedWidth": 297,
|
||||||
|
"SelectedChildIndex": 2,
|
||||||
|
"Children": [
|
||||||
|
{
|
||||||
|
"$type": "Bookmark",
|
||||||
|
"Name": "ST:0:0:{57d563b6-44a5-47df-85be-f4199ad6b651}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$type": "Document",
|
||||||
|
"DocumentIndex": 2,
|
||||||
|
"Title": "ProfileSwitcher.cs",
|
||||||
|
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\ProfileSwitcher.cs",
|
||||||
|
"RelativeDocumentMoniker": "ClipTrimDotNet\\ProfileSwitcher.cs",
|
||||||
|
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\ProfileSwitcher.cs",
|
||||||
|
"RelativeToolTip": "ClipTrimDotNet\\ProfileSwitcher.cs",
|
||||||
|
"ViewState": "AgIAAFkAAAAAAAAAAAAlwG8AAABKAAAAAAAAAA==",
|
||||||
|
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||||
|
"WhenOpened": "2026-02-21T15:06:24.045Z",
|
||||||
|
"EditorCaption": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$type": "Document",
|
||||||
|
"DocumentIndex": 0,
|
||||||
|
"Title": "ClipTrimClient.cs",
|
||||||
|
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Client\\ClipTrimClient.cs",
|
||||||
|
"RelativeDocumentMoniker": "ClipTrimDotNet\\Client\\ClipTrimClient.cs",
|
||||||
|
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Client\\ClipTrimClient.cs",
|
||||||
|
"RelativeToolTip": "ClipTrimDotNet\\Client\\ClipTrimClient.cs",
|
||||||
|
"ViewState": "AgIAAEgAAAAAAAAAAAAuwGMAAAAJAAAAAAAAAA==",
|
||||||
|
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||||
|
"WhenOpened": "2026-02-21T15:03:49.814Z",
|
||||||
|
"EditorCaption": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$type": "Document",
|
||||||
|
"DocumentIndex": 3,
|
||||||
|
"Title": "CollectionMetaData.cs",
|
||||||
|
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Client\\CollectionMetaData.cs",
|
||||||
|
"RelativeDocumentMoniker": "ClipTrimDotNet\\Client\\CollectionMetaData.cs",
|
||||||
|
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Client\\CollectionMetaData.cs",
|
||||||
|
"RelativeToolTip": "ClipTrimDotNet\\Client\\CollectionMetaData.cs",
|
||||||
|
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||||
|
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||||
|
"WhenOpened": "2026-02-21T15:03:47.862Z",
|
||||||
|
"EditorCaption": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$type": "Document",
|
||||||
|
"DocumentIndex": 1,
|
||||||
|
"Title": "Player.cs",
|
||||||
|
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Player.cs",
|
||||||
|
"RelativeDocumentMoniker": "ClipTrimDotNet\\Player.cs",
|
||||||
|
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Player.cs*",
|
||||||
|
"RelativeToolTip": "ClipTrimDotNet\\Player.cs*",
|
||||||
|
"ViewState": "AgIAAHIAAAAAAAAAAAA3wIYAAABMAAAAAAAAAA==",
|
||||||
|
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||||
|
"WhenOpened": "2026-02-21T15:00:23.762Z",
|
||||||
|
"EditorCaption": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$type": "Bookmark",
|
||||||
|
"Name": "ST:0:0:{cce594b6-0c39-4442-ba28-10c64ac7e89f}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
113
stream_deck_plugin/.vs/ClipTrimDotNet/v17/DocumentLayout.json
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
{
|
||||||
|
"Version": 1,
|
||||||
|
"WorkspaceRootPath": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\",
|
||||||
|
"Documents": [
|
||||||
|
{
|
||||||
|
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\wavplayer.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||||
|
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\wavplayer.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\player.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||||
|
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\player.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\profileswitcher.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||||
|
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\profileswitcher.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\client\\cliptrimclient.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||||
|
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\client\\cliptrimclient.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\client\\collectionmetadata.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
|
||||||
|
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\client\\collectionmetadata.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"DocumentGroupContainers": [
|
||||||
|
{
|
||||||
|
"Orientation": 0,
|
||||||
|
"VerticalTabListWidth": 256,
|
||||||
|
"DocumentGroups": [
|
||||||
|
{
|
||||||
|
"DockedWidth": 297,
|
||||||
|
"SelectedChildIndex": 1,
|
||||||
|
"Children": [
|
||||||
|
{
|
||||||
|
"$type": "Bookmark",
|
||||||
|
"Name": "ST:0:0:{57d563b6-44a5-47df-85be-f4199ad6b651}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$type": "Document",
|
||||||
|
"DocumentIndex": 0,
|
||||||
|
"Title": "WavPlayer.cs",
|
||||||
|
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\WavPlayer.cs",
|
||||||
|
"RelativeDocumentMoniker": "ClipTrimDotNet\\WavPlayer.cs",
|
||||||
|
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\WavPlayer.cs",
|
||||||
|
"RelativeToolTip": "ClipTrimDotNet\\WavPlayer.cs",
|
||||||
|
"ViewState": "AgIAALYAAAAAAAAAAAAAALsAAAANAAAAAAAAAA==",
|
||||||
|
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||||
|
"WhenOpened": "2026-02-21T15:16:26.477Z",
|
||||||
|
"EditorCaption": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$type": "Document",
|
||||||
|
"DocumentIndex": 2,
|
||||||
|
"Title": "ProfileSwitcher.cs",
|
||||||
|
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\ProfileSwitcher.cs",
|
||||||
|
"RelativeDocumentMoniker": "ClipTrimDotNet\\ProfileSwitcher.cs",
|
||||||
|
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\ProfileSwitcher.cs",
|
||||||
|
"RelativeToolTip": "ClipTrimDotNet\\ProfileSwitcher.cs",
|
||||||
|
"ViewState": "AgIAAG8AAAAAAAAAAAAWwG8AAABKAAAAAAAAAA==",
|
||||||
|
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||||
|
"WhenOpened": "2026-02-21T15:06:24.045Z",
|
||||||
|
"EditorCaption": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$type": "Document",
|
||||||
|
"DocumentIndex": 3,
|
||||||
|
"Title": "ClipTrimClient.cs",
|
||||||
|
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Client\\ClipTrimClient.cs",
|
||||||
|
"RelativeDocumentMoniker": "ClipTrimDotNet\\Client\\ClipTrimClient.cs",
|
||||||
|
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Client\\ClipTrimClient.cs",
|
||||||
|
"RelativeToolTip": "ClipTrimDotNet\\Client\\ClipTrimClient.cs",
|
||||||
|
"ViewState": "AgIAAEgAAAAAAAAAAAAuwGIAAAApAAAAAAAAAA==",
|
||||||
|
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||||
|
"WhenOpened": "2026-02-21T15:03:49.814Z",
|
||||||
|
"EditorCaption": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$type": "Document",
|
||||||
|
"DocumentIndex": 4,
|
||||||
|
"Title": "CollectionMetaData.cs",
|
||||||
|
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Client\\CollectionMetaData.cs",
|
||||||
|
"RelativeDocumentMoniker": "ClipTrimDotNet\\Client\\CollectionMetaData.cs",
|
||||||
|
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Client\\CollectionMetaData.cs",
|
||||||
|
"RelativeToolTip": "ClipTrimDotNet\\Client\\CollectionMetaData.cs",
|
||||||
|
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
|
||||||
|
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||||
|
"WhenOpened": "2026-02-21T15:03:47.862Z",
|
||||||
|
"EditorCaption": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$type": "Document",
|
||||||
|
"DocumentIndex": 1,
|
||||||
|
"Title": "Player.cs",
|
||||||
|
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Player.cs",
|
||||||
|
"RelativeDocumentMoniker": "ClipTrimDotNet\\Player.cs",
|
||||||
|
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Player.cs",
|
||||||
|
"RelativeToolTip": "ClipTrimDotNet\\Player.cs",
|
||||||
|
"ViewState": "AgIAAHoAAAAAAAAAAAAswIwAAAAbAAAAAAAAAA==",
|
||||||
|
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
|
||||||
|
"WhenOpened": "2026-02-21T15:00:23.762Z",
|
||||||
|
"EditorCaption": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$type": "Bookmark",
|
||||||
|
"Name": "ST:0:0:{cce594b6-0c39-4442-ba28-10c64ac7e89f}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
25
stream_deck_plugin/ClipTrimDotNet.sln
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.8.34330.188
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClipTrimDotNet", "ClipTrimDotNet\ClipTrimDotNet.csproj", "{4635D874-69C0-4010-BE46-77EF92EB1553}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{4635D874-69C0-4010-BE46-77EF92EB1553}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{4635D874-69C0-4010-BE46-77EF92EB1553}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{4635D874-69C0-4010-BE46-77EF92EB1553}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{4635D874-69C0-4010-BE46-77EF92EB1553}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {926C6896-F36A-4F3B-A9DD-CA3AA48AD99F}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
14
stream_deck_plugin/ClipTrimDotNet/!!README!!.txt
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
To use:
|
||||||
|
1. Right click the project and choose "Manage Nuget Packages"
|
||||||
|
2. Choose the restore option in the Nuget screen (or just install the latest StreamDeck-Tools from Nuget)
|
||||||
|
3. Update the manifest.json file with the correct details about your plugin
|
||||||
|
4. Modify PluginAction.cs as needed (it holds the logic for your plugin)
|
||||||
|
5. Modify the PropertyInspector\PluginActionPI.html and PropertyInspector\PluginActionPI.js as needed to show field in the Property Inspector
|
||||||
|
6. Before releasing, change the Assembly Information (Right click the project -> Properties -> Application -> Assembly Information...)
|
||||||
|
|
||||||
|
For help with StreamDeck-Tools:
|
||||||
|
Discord Server: http://discord.barraider.com
|
||||||
|
Resources:
|
||||||
|
* StreamDeck-Tools samples and tutorial: https://github.com/BarRaider/streamdeck-tools
|
||||||
|
* EasyPI library (for working with Property Inspector): https://github.com/BarRaider/streamdeck-easypi
|
||||||
|
|
||||||
22
stream_deck_plugin/ClipTrimDotNet/App.config
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<startup>
|
||||||
|
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8"/>
|
||||||
|
</startup>
|
||||||
|
<runtime>
|
||||||
|
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="CommandLine" publicKeyToken="5a870481e358d379" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-2.6.0.0" newVersion="2.6.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="System.Drawing.Common" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral"/>
|
||||||
|
<bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</assemblyBinding>
|
||||||
|
</runtime>
|
||||||
|
</configuration>
|
||||||
1
stream_deck_plugin/ClipTrimDotNet/BaseTest.cs
Normal file
@ -0,0 +1 @@
|
|||||||
|
|
||||||
42
stream_deck_plugin/ClipTrimDotNet/Client/ClipMetadata.cs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace ClipTrimDotNet.Client
|
||||||
|
{
|
||||||
|
public enum PlaybackType
|
||||||
|
{
|
||||||
|
playStop,
|
||||||
|
playOverlap
|
||||||
|
}
|
||||||
|
public class ClipMetadata
|
||||||
|
{
|
||||||
|
[JsonProperty(PropertyName = "filename")]
|
||||||
|
public string Filename { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "name")]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "volume")]
|
||||||
|
public double Volume { get; set; } = 1.0;
|
||||||
|
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "startTime")]
|
||||||
|
public double StartTime { get; set; } = 0.0;
|
||||||
|
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "endTime")]
|
||||||
|
public double EndTime { get; set; } = 0.0;
|
||||||
|
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "playbackType")]
|
||||||
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
|
public PlaybackType PlaybackType { get; set; } = PlaybackType.playStop;
|
||||||
|
}
|
||||||
|
}
|
||||||
110
stream_deck_plugin/ClipTrimDotNet/Client/ClipTrimClient.cs
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace ClipTrimDotNet.Client
|
||||||
|
{
|
||||||
|
public class ClipTrimClient
|
||||||
|
{
|
||||||
|
private static ClipTrimClient? instance;
|
||||||
|
public static ClipTrimClient Instance
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (instance == null)
|
||||||
|
{
|
||||||
|
instance = new ClipTrimClient();
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpClient httpClient;
|
||||||
|
|
||||||
|
public ClipTrimClient()
|
||||||
|
{
|
||||||
|
httpClient = new HttpClient()
|
||||||
|
{
|
||||||
|
BaseAddress = new Uri("http://localhost:5010/"),
|
||||||
|
Timeout = TimeSpan.FromSeconds(10)
|
||||||
|
};
|
||||||
|
Task.Run(ShortPoll);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ShortPoll()
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
await GetMetadata();
|
||||||
|
await Task.Delay(TimeSpan.FromSeconds(5)); await Task.Delay(TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CollectionMetaData> Collections { get; private set; } = new List<CollectionMetaData>();
|
||||||
|
public CollectionMetaData? SelectedCollection { get; private set; }
|
||||||
|
public int PageIndex { get; private set; } = 0;
|
||||||
|
private async Task GetMetadata()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await httpClient.GetAsync("meta");
|
||||||
|
if (response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
var json = await response.Content.ReadAsStringAsync();
|
||||||
|
dynamic collections = JsonConvert.DeserializeObject(json);
|
||||||
|
collections = collections.collections;
|
||||||
|
Collections = JsonConvert.DeserializeObject<List<CollectionMetaData>>(collections.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Error pinging ClipTrim API: {ex.Message}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> GetCollectionNames()
|
||||||
|
{
|
||||||
|
//await GetMetadata();
|
||||||
|
return Collections.Select(x => x.Name).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetSelectedCollectionByName(string name)
|
||||||
|
{
|
||||||
|
var collection = Collections.FirstOrDefault(x => x.Name == name);
|
||||||
|
if (collection != null)
|
||||||
|
{
|
||||||
|
SelectedCollection = collection;
|
||||||
|
PageIndex = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClipMetadata? GetClipByPagedIndex(int index)
|
||||||
|
{
|
||||||
|
if (SelectedCollection == null) return null;
|
||||||
|
int clipIndex = PageIndex * 10 + index;
|
||||||
|
if (clipIndex >= 0 && clipIndex < SelectedCollection.Clips.Count)
|
||||||
|
{
|
||||||
|
return SelectedCollection.Clips[clipIndex];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void PlayClip(ClipMetadata? metadata)
|
||||||
|
{
|
||||||
|
if (metadata == null) return;
|
||||||
|
|
||||||
|
var response = await httpClient.PostAsync("playback/start", new StringContent(JsonConvert.SerializeObject(metadata), Encoding.UTF8, "application/json"));
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Error playing clip: {response.ReasonPhrase}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace ClipTrimDotNet.Client
|
||||||
|
{
|
||||||
|
public class CollectionMetaData
|
||||||
|
{
|
||||||
|
[JsonProperty(PropertyName = "name")]
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "clips")]
|
||||||
|
public List<ClipMetadata> Clips { get; set; } = new List<ClipMetadata>();
|
||||||
|
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "id")]
|
||||||
|
public int Id { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
162
stream_deck_plugin/ClipTrimDotNet/ClipTrimDotNet.csproj
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||||
|
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||||
|
<ProjectGuid>{4635D874-69C0-4010-BE46-77EF92EB1553}</ProjectGuid>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<RootNamespace>ClipTrimDotNet</RootNamespace>
|
||||||
|
<AssemblyName>ClipTrimDotNet</AssemblyName>
|
||||||
|
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
||||||
|
<LangVersion>8</LangVersion>
|
||||||
|
<FileAlignment>512</FileAlignment>
|
||||||
|
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||||
|
<Deterministic>true</Deterministic>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<TargetFrameworkProfile />
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<DebugSymbols>true</DebugSymbols>
|
||||||
|
<DebugType>full</DebugType>
|
||||||
|
<Optimize>false</Optimize>
|
||||||
|
<OutputPath>bin\Debug\com.michal-courson.cliptrim.sdPlugin\</OutputPath>
|
||||||
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||||
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
|
<DebugType>pdbonly</DebugType>
|
||||||
|
<Optimize>true</Optimize>
|
||||||
|
<OutputPath>bin\Release\ClipTrimDotNet.sdPlugin\</OutputPath>
|
||||||
|
<DefineConstants>TRACE</DefineConstants>
|
||||||
|
<ErrorReport>prompt</ErrorReport>
|
||||||
|
<WarningLevel>4</WarningLevel>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="CommandLine, Version=2.9.1.0, Culture=neutral, PublicKeyToken=5a870481e358d379, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\CommandLineParser.2.9.1\lib\net461\CommandLine.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="Microsoft.Win32.Registry, Version=4.1.3.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\Microsoft.Win32.Registry.4.7.0\lib\net461\Microsoft.Win32.Registry.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="NAudio, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\NAudio.2.2.1\lib\net472\NAudio.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="NAudio.Asio, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\NAudio.Asio.2.2.1\lib\netstandard2.0\NAudio.Asio.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="NAudio.Core, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\NAudio.Core.2.2.1\lib\netstandard2.0\NAudio.Core.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="NAudio.Midi, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\NAudio.Midi.2.2.1\lib\netstandard2.0\NAudio.Midi.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="NAudio.Wasapi, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\NAudio.Wasapi.2.2.1\lib\netstandard2.0\NAudio.Wasapi.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="NAudio.WinForms, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\NAudio.WinForms.2.2.1\lib\net472\NAudio.WinForms.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="NAudio.WinMM, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\NAudio.WinMM.2.2.1\lib\netstandard2.0\NAudio.WinMM.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\Newtonsoft.Json.13.0.4\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="NLog, Version=6.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\NLog.6.0.5\lib\net46\NLog.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="StreamDeckTools, Version=6.3.2.0, Culture=neutral, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\StreamDeck-Tools.6.3.2\lib\netstandard2.0\StreamDeckTools.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System" />
|
||||||
|
<Reference Include="System.Configuration" />
|
||||||
|
<Reference Include="System.Core" />
|
||||||
|
<Reference Include="System.Drawing" />
|
||||||
|
<Reference Include="System.Drawing.Common, Version=9.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\System.Drawing.Common.9.0.10\lib\net462\System.Drawing.Common.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.IO.Compression" />
|
||||||
|
<Reference Include="System.Runtime.Serialization" />
|
||||||
|
<Reference Include="System.Security.AccessControl, Version=4.1.3.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\System.Security.AccessControl.4.7.0\lib\net461\System.Security.AccessControl.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.Security.Principal.Windows, Version=4.1.3.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
|
||||||
|
<HintPath>..\packages\System.Security.Principal.Windows.4.7.0\lib\net461\System.Security.Principal.Windows.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="System.ServiceModel" />
|
||||||
|
<Reference Include="System.Transactions" />
|
||||||
|
<Reference Include="System.Xml.Linq" />
|
||||||
|
<Reference Include="System.Data.DataSetExtensions" />
|
||||||
|
<Reference Include="Microsoft.CSharp" />
|
||||||
|
<Reference Include="System.Data" />
|
||||||
|
<Reference Include="System.Net.Http" />
|
||||||
|
<Reference Include="System.Xml" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="BaseTest.cs" />
|
||||||
|
<Compile Include="Client\ClipMetadata.cs" />
|
||||||
|
<Compile Include="Client\ClipTrimClient.cs" />
|
||||||
|
<Compile Include="Client\CollectionMetaData.cs" />
|
||||||
|
<Compile Include="GlobalSettings.cs" />
|
||||||
|
<Compile Include="Player.cs" />
|
||||||
|
<Compile Include="ProfileSwitcher.cs" />
|
||||||
|
<Compile Include="Program.cs" />
|
||||||
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
|
<Compile Include="WavPlayer.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Include="App.config" />
|
||||||
|
<None Include="DialLayout.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Include="manifest.json">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
<None Include="packages.config" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="!!README!!.txt" />
|
||||||
|
<Content Include="Images\categoryIcon%402x.png">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="Images\categoryIcon.png">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="Images\icon%402x.png">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="Images\icon.png">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="Images\pluginAction%402x.png">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="Images\pluginAction.png">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="Images\pluginIcon%402x.png">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="Images\pluginIcon.png">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="package.json" />
|
||||||
|
<Content Include="PropertyInspector\profile_swticher.html">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
<Content Include="PropertyInspector\file_player.html">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
|
<PropertyGroup>
|
||||||
|
<PreBuildEvent>npm run stop</PreBuildEvent>
|
||||||
|
</PropertyGroup>
|
||||||
|
<PropertyGroup>
|
||||||
|
<PostBuildEvent>npm run start</PostBuildEvent>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|AnyCPU'">
|
||||||
|
<StartArguments>--port 23654 --pluginUUID com.michal-courson.cliptrim --registerEvent restart --info {}</StartArguments>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
40
stream_deck_plugin/ClipTrimDotNet/DialLayout.json
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"id": "sampleDial",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"key": "title",
|
||||||
|
"type": "text",
|
||||||
|
"rect": [ 16, 10, 136, 24 ],
|
||||||
|
"font": {
|
||||||
|
"size": 16,
|
||||||
|
"weight": 600
|
||||||
|
},
|
||||||
|
"alignment": "left"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "icon",
|
||||||
|
"type": "pixmap",
|
||||||
|
"rect": [ 16, 40, 48, 48 ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "value",
|
||||||
|
"type": "text",
|
||||||
|
"rect": [ 76, 40, 108, 32 ],
|
||||||
|
"font": {
|
||||||
|
"size": 24,
|
||||||
|
"weight": 600
|
||||||
|
},
|
||||||
|
"alignment": "right"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "indicator",
|
||||||
|
"type": "gbar",
|
||||||
|
"rect": [ 76, 74, 108, 20 ],
|
||||||
|
"value": 0,
|
||||||
|
"subtype": 4,
|
||||||
|
"bar_h": 12,
|
||||||
|
"border_w": 0,
|
||||||
|
"bar_bg_c": "0:#427018,0.75:#705B1C,0.90:#702735,1:#702735"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
109
stream_deck_plugin/ClipTrimDotNet/GlobalSettings.cs
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
using BarRaider.SdTools;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BarRaider.SdTools.Wrappers;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using NAudio.MediaFoundation;
|
||||||
|
|
||||||
|
namespace ClipTrimDotNet
|
||||||
|
{
|
||||||
|
public class FileEntry
|
||||||
|
{
|
||||||
|
public FileEntry()
|
||||||
|
{
|
||||||
|
Volume = 1.0;
|
||||||
|
Playtype = "Play/Overlap";
|
||||||
|
}
|
||||||
|
[JsonProperty(PropertyName = "Volume")]
|
||||||
|
public double Volume { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "Playtype")]
|
||||||
|
public string Playtype { get; set; }
|
||||||
|
}
|
||||||
|
public class CollectionEntry
|
||||||
|
{
|
||||||
|
public CollectionEntry()
|
||||||
|
{
|
||||||
|
Files = new Dictionary<string, FileEntry>();
|
||||||
|
}
|
||||||
|
[JsonProperty(PropertyName = "Files")]
|
||||||
|
public Dictionary<string, FileEntry> Files { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class GlobalSettings
|
||||||
|
{
|
||||||
|
public static GlobalSettings? _inst;
|
||||||
|
public static GlobalSettings Instance
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
_inst ??= CreateDefaultSettings();
|
||||||
|
return _inst;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_inst = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public static GlobalSettings CreateDefaultSettings()
|
||||||
|
{
|
||||||
|
GlobalSettings instance = new GlobalSettings();
|
||||||
|
instance.BasePath = null;
|
||||||
|
instance.ProfileName = null;
|
||||||
|
instance.Collections = new Dictionary<string, CollectionEntry>();
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[FilenameProperty]
|
||||||
|
[JsonProperty(PropertyName = "basePath")]
|
||||||
|
public string? BasePath { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "profileName")]
|
||||||
|
public string? ProfileName { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "outputDevice")]
|
||||||
|
public string? OutputDevice { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "collections")]
|
||||||
|
public Dictionary<string, CollectionEntry> Collections { get; set; }
|
||||||
|
|
||||||
|
public void SetCurrentProfile(string profile)
|
||||||
|
{
|
||||||
|
ProfileName = profile;
|
||||||
|
if(!Collections.ContainsKey(profile))
|
||||||
|
{
|
||||||
|
Collections.Add(profile, new CollectionEntry());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileEntry GetFileOptionsInCurrentProfile(string filename)
|
||||||
|
{
|
||||||
|
if(!Collections.TryGetValue(ProfileName, out CollectionEntry collection))
|
||||||
|
{
|
||||||
|
return new FileEntry();
|
||||||
|
}
|
||||||
|
if(!collection.Files.TryGetValue(filename, out FileEntry file))
|
||||||
|
{
|
||||||
|
return new FileEntry();
|
||||||
|
}
|
||||||
|
Logger.Instance.LogMessage(TracingLevel.INFO, "fetched file settings " + filename + JsonConvert.SerializeObject(file));
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetFileOptionsCurrentProfile(string filename, FileEntry file)
|
||||||
|
{
|
||||||
|
Logger.Instance.LogMessage(TracingLevel.INFO, "SetFileOptionsCurrentProfile ");
|
||||||
|
if (!Collections.TryGetValue(ProfileName, out CollectionEntry collection))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Logger.Instance.LogMessage(TracingLevel.INFO, "SetFileOptionsCurrentProfile 2");
|
||||||
|
//collection.Files[filename] = file;
|
||||||
|
Collections[ProfileName].Files[filename] = file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
stream_deck_plugin/ClipTrimDotNet/Images/categoryIcon.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
stream_deck_plugin/ClipTrimDotNet/Images/categoryIcon@2x.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
stream_deck_plugin/ClipTrimDotNet/Images/icon.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
BIN
stream_deck_plugin/ClipTrimDotNet/Images/icon@2x.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
stream_deck_plugin/ClipTrimDotNet/Images/off_save.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
stream_deck_plugin/ClipTrimDotNet/Images/pluginAction.png
Normal file
|
After Width: | Height: | Size: 8.1 KiB |
BIN
stream_deck_plugin/ClipTrimDotNet/Images/pluginAction@2x.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
stream_deck_plugin/ClipTrimDotNet/Images/pluginIcon.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
stream_deck_plugin/ClipTrimDotNet/Images/pluginIcon@2x.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
193
stream_deck_plugin/ClipTrimDotNet/Player.cs
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
using BarRaider.SdTools;
|
||||||
|
using BarRaider.SdTools.Wrappers;
|
||||||
|
using ClipTrimDotNet.Client;
|
||||||
|
using NAudio.CoreAudioApi.Interfaces;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Dynamic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace ClipTrimDotNet
|
||||||
|
{
|
||||||
|
[PluginActionId("com.michal-courson.cliptrim.player")]
|
||||||
|
public class Player : KeypadBase
|
||||||
|
{
|
||||||
|
|
||||||
|
private ClipMetadata? metadata;
|
||||||
|
private KeyCoordinates coordinates;
|
||||||
|
private class PluginSettings
|
||||||
|
{
|
||||||
|
public static PluginSettings CreateDefaultSettings()
|
||||||
|
{
|
||||||
|
PluginSettings instance = new PluginSettings();
|
||||||
|
instance.Path = null;
|
||||||
|
instance.PlayType = "Play/Overlap";
|
||||||
|
instance.Index = 0;
|
||||||
|
instance.Volume = 1;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
[FilenameProperty]
|
||||||
|
[JsonProperty(PropertyName = "path")]
|
||||||
|
public string? Path { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "index")]
|
||||||
|
public int? Index { get; set; }
|
||||||
|
[JsonProperty(PropertyName = "playtype")]
|
||||||
|
public string PlayType { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "volume")]
|
||||||
|
public double Volume { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Private Members
|
||||||
|
|
||||||
|
private PluginSettings settings;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
public Player(SDConnection connection, InitialPayload payload) : base(connection, payload)
|
||||||
|
{
|
||||||
|
if (payload.Settings == null || payload.Settings.Count == 0)
|
||||||
|
{
|
||||||
|
this.settings = PluginSettings.CreateDefaultSettings();
|
||||||
|
SaveSettings();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.settings = payload.Settings.ToObject<PluginSettings>();
|
||||||
|
}
|
||||||
|
this.coordinates = payload.Coordinates;
|
||||||
|
GlobalSettingsManager.Instance.RequestGlobalSettings();
|
||||||
|
CheckFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Instance_OnReceivedGlobalSettings(object sender, ReceivedGlobalSettingsPayload e)
|
||||||
|
{
|
||||||
|
Tools.AutoPopulateSettings(GlobalSettings.Instance, e.Settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetIndex()
|
||||||
|
{
|
||||||
|
return Math.Max((coordinates.Row - 1) * 5 + coordinates.Column, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void CheckFile()
|
||||||
|
{
|
||||||
|
|
||||||
|
//if (settings == null || GlobalSettings.Instance.ProfileName ==null) return;
|
||||||
|
metadata = ClipTrimClient.Instance.GetClipByPagedIndex(GetIndex());
|
||||||
|
await Connection.SetTitleAsync($"{metadata?.Name ?? ""}");
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
//var files = Directory.GetFiles(Path.Combine(Path.GetDirectoryName(GlobalSettings.Instance.BasePath), GlobalSettings.Instance.ProfileName), "*.wav", SearchOption.TopDirectoryOnly)
|
||||||
|
// .OrderBy(file => File.GetCreationTime(file))
|
||||||
|
// .ToArray();
|
||||||
|
//int? i = this.settings.Index;
|
||||||
|
//string new_path = "";
|
||||||
|
//if (i != null && i >= 0 && i < files.Length)
|
||||||
|
//{
|
||||||
|
// new_path = files[i ?? 0];
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
//await Connection.SetTitleAsync(Path.GetFileNameWithoutExtension(new_path));
|
||||||
|
//if (new_path != settings.Path)
|
||||||
|
//{
|
||||||
|
// settings.Path = new_path;
|
||||||
|
// if(new_path != "")
|
||||||
|
// {
|
||||||
|
// FileEntry opts = GlobalSettings.Instance.GetFileOptionsInCurrentProfile(new_path);
|
||||||
|
// settings.Volume = opts.Volume;
|
||||||
|
// settings.PlayType = opts.Playtype;
|
||||||
|
// }
|
||||||
|
// await SaveSettings();
|
||||||
|
//}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private async void Connection_OnApplicationDidLaunch(object sender, BarRaider.SdTools.Wrappers.SDEventReceivedEventArgs<BarRaider.SdTools.Events.ApplicationDidLaunch> e)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Connection_OnTitleParametersDidChange(object sender, SDEventReceivedEventArgs<BarRaider.SdTools.Events.TitleParametersDidChange> e)
|
||||||
|
{
|
||||||
|
//titleParameters = e.Event?.Payload?.TitleParameters;
|
||||||
|
//userTitle = e.Event?.Payload?.Title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Dispose()
|
||||||
|
{
|
||||||
|
Connection.OnTitleParametersDidChange -= Connection_OnTitleParametersDidChange;
|
||||||
|
Connection.OnApplicationDidLaunch -= Connection_OnApplicationDidLaunch;
|
||||||
|
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Destructor called");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void KeyPressed(KeyPayload payload)
|
||||||
|
{
|
||||||
|
//Logger.Instance.LogMessage(TracingLevel.INFO, "Key Pressedd");
|
||||||
|
Tools.AutoPopulateSettings(settings, payload.Settings);
|
||||||
|
// Logger.Instance.LogMessage(TracingLevel.INFO, JsonConvert.SerializeObject(settings));
|
||||||
|
ClipTrimClient.Instance.PlayClip(metadata);
|
||||||
|
//try
|
||||||
|
//{
|
||||||
|
// WavPlayer.Instance.Play(settings.Path, GlobalSettings.Instance.OutputDevice, settings.Volume, settings.PlayType == "Play/Overlap" ? WavPlayer.PlayMode.PlayOverlap : WavPlayer.PlayMode.PlayStop);
|
||||||
|
//}
|
||||||
|
//catch
|
||||||
|
//{
|
||||||
|
|
||||||
|
//}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void KeyReleased(KeyPayload payload) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnTick() {
|
||||||
|
CheckFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async void ReceivedSettings(ReceivedSettingsPayload payload)
|
||||||
|
{
|
||||||
|
Logger.Instance.LogMessage(TracingLevel.INFO, "Player rec settings");
|
||||||
|
Tools.AutoPopulateSettings(settings, payload.Settings);
|
||||||
|
GlobalSettings.Instance.SetFileOptionsCurrentProfile(settings.Path, new FileEntry() { Playtype = settings.PlayType, Volume = settings.Volume });
|
||||||
|
await Connection.SetGlobalSettingsAsync(JObject.FromObject(GlobalSettings.Instance));
|
||||||
|
//SaveSettings();
|
||||||
|
//CheckFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload) {
|
||||||
|
//Logger.Instance.LogMessage(TracingLevel.INFO, "ReceivedGlobalSettings");
|
||||||
|
|
||||||
|
if (payload.Settings == null || payload.Settings.Count == 0)
|
||||||
|
{
|
||||||
|
var inst = GlobalSettings.Instance;
|
||||||
|
//GlobalSettingsManager.Instance.SetGlobalSettings(JObject.FromObject(inst));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GlobalSettings.Instance = payload.Settings.ToObject<GlobalSettings>();
|
||||||
|
}
|
||||||
|
|
||||||
|
//CheckFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Private Methods
|
||||||
|
|
||||||
|
private Task SaveSettings()
|
||||||
|
{
|
||||||
|
return Connection.SetSettingsAsync(JObject.FromObject(settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
167
stream_deck_plugin/ClipTrimDotNet/ProfileSwitcher.cs
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
using BarRaider.SdTools;
|
||||||
|
using BarRaider.SdTools.Wrappers;
|
||||||
|
using ClipTrimDotNet.Client;
|
||||||
|
using NAudio.CoreAudioApi.Interfaces;
|
||||||
|
using NAudio.Wave;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Dynamic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace ClipTrimDotNet
|
||||||
|
{
|
||||||
|
public class DataSourceItem
|
||||||
|
{
|
||||||
|
public string label { get; set; }
|
||||||
|
public string value { get; set; }
|
||||||
|
}
|
||||||
|
[PluginActionId("com.michal-courson.cliptrim.profile-switcher")]
|
||||||
|
public class ProfileSwitcher : KeypadBase
|
||||||
|
{
|
||||||
|
private class PluginSettings
|
||||||
|
{
|
||||||
|
public static PluginSettings CreateDefaultSettings()
|
||||||
|
{
|
||||||
|
PluginSettings instance = new PluginSettings();
|
||||||
|
instance.ProfileName = null;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonProperty(PropertyName = "profileName")]
|
||||||
|
public string? ProfileName { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Private Members
|
||||||
|
|
||||||
|
private PluginSettings settings;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
public ProfileSwitcher(SDConnection connection, InitialPayload payload) : base(connection, payload)
|
||||||
|
{
|
||||||
|
if (payload.Settings == null || payload.Settings.Count == 0)
|
||||||
|
{
|
||||||
|
this.settings = PluginSettings.CreateDefaultSettings();
|
||||||
|
SaveSettings();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
this.settings = payload.Settings.ToObject<PluginSettings>();
|
||||||
|
}
|
||||||
|
GlobalSettingsManager.Instance.RequestGlobalSettings();
|
||||||
|
Connection.OnSendToPlugin += Connection_OnSendToPlugin;
|
||||||
|
SetTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void SetTitle()
|
||||||
|
{
|
||||||
|
|
||||||
|
await Connection.SetTitleAsync(settings.ProfileName + " A");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Connection_OnSendToPlugin(object sender, SDEventReceivedEventArgs<BarRaider.SdTools.Events.SendToPlugin> e)
|
||||||
|
{
|
||||||
|
//Logger.Instance.LogMessage(TracingLevel.INFO, "get profiles");
|
||||||
|
if (e.Event.Payload["event"].ToString() == "getProfiles")
|
||||||
|
{
|
||||||
|
//string basePath = "C:\\Users\\mickl\\Music\\clips";
|
||||||
|
//var files = Directory.GetDirectories(basePath, "*", SearchOption.TopDirectoryOnly).Select(x => Path.GetFileNameWithoutExtension(x)).Where(x => x != "original");
|
||||||
|
var files = ClipTrimClient.Instance.GetCollectionNames();
|
||||||
|
var items = files.Select(x => new DataSourceItem { label = x, value = x});
|
||||||
|
var obj = new JObject();
|
||||||
|
obj["event"] = "getProfiles";
|
||||||
|
obj["items"] = JArray.FromObject(items);
|
||||||
|
//Logger.Instance.LogMessage(TracingLevel.INFO, "get profiles return " + JsonConvert.SerializeObject(obj));
|
||||||
|
await Connection.SendToPropertyInspectorAsync(obj);
|
||||||
|
}
|
||||||
|
if (e.Event.Payload["event"].ToString() == "getOutputDevices")
|
||||||
|
{
|
||||||
|
List<WaveOutCapabilities> devices = new List<WaveOutCapabilities>();
|
||||||
|
for (int n = -1; n < WaveOut.DeviceCount; n++)
|
||||||
|
{
|
||||||
|
var caps = WaveOut.GetCapabilities(n);
|
||||||
|
devices.Add(caps);
|
||||||
|
}
|
||||||
|
var items = devices.Select(x => new DataSourceItem { label = x.ProductName, value = x.ProductName });
|
||||||
|
var obj = new JObject();
|
||||||
|
obj["event"] = "getOutputDevices";
|
||||||
|
obj["items"] = JArray.FromObject(items);
|
||||||
|
//Logger.Instance.LogMessage(TracingLevel.INFO, "get devices return " + JsonConvert.SerializeObject(obj));
|
||||||
|
await Connection.SendToPropertyInspectorAsync(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Connection_OnTitleParametersDidChange(object sender, SDEventReceivedEventArgs<BarRaider.SdTools.Events.TitleParametersDidChange> e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Dispose()
|
||||||
|
{
|
||||||
|
Connection.OnTitleParametersDidChange -= Connection_OnTitleParametersDidChange;
|
||||||
|
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Destructor called");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async void KeyPressed(KeyPayload payload)
|
||||||
|
{
|
||||||
|
//Logger.Instance.LogMessage(TracingLevel.INFO, "KeyPressed");
|
||||||
|
//Logger.Instance.LogMessage(TracingLevel.INFO, JsonConvert.SerializeObject(settings));
|
||||||
|
//Logger.Instance.LogMessage(TracingLevel.INFO, JsonConvert.SerializeObject(GlobalSettings.Instance));
|
||||||
|
ClipTrimClient.Instance.SetSelectedCollectionByName(settings.ProfileName);
|
||||||
|
GlobalSettings.Instance.SetCurrentProfile(settings.ProfileName);
|
||||||
|
Logger.Instance.LogMessage(TracingLevel.INFO, JsonConvert.SerializeObject(GlobalSettings.Instance));
|
||||||
|
|
||||||
|
await Connection.SetGlobalSettingsAsync(JObject.FromObject(GlobalSettings.Instance));
|
||||||
|
await Connection.SwitchProfileAsync("ClipTrim");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void KeyReleased(KeyPayload payload)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OnTick()
|
||||||
|
{
|
||||||
|
SetTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ReceivedSettings(ReceivedSettingsPayload payload)
|
||||||
|
{
|
||||||
|
Tools.AutoPopulateSettings(settings, payload.Settings);
|
||||||
|
SaveSettings();
|
||||||
|
//tTitle();
|
||||||
|
//CheckFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload)
|
||||||
|
{
|
||||||
|
//Logger.Instance.LogMessage(TracingLevel.INFO, "ReceivedGlobalSettings");
|
||||||
|
|
||||||
|
if (payload.Settings == null || payload.Settings.Count == 0)
|
||||||
|
{
|
||||||
|
var inst = GlobalSettings.Instance;
|
||||||
|
//GlobalSettingsManager.Instance.SetGlobalSettings(JObject.FromObject(inst));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GlobalSettings.Instance = payload.Settings.ToObject<GlobalSettings>();
|
||||||
|
}
|
||||||
|
|
||||||
|
//CheckFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Private Methods
|
||||||
|
|
||||||
|
private Task SaveSettings()
|
||||||
|
{
|
||||||
|
return Connection.SetSettingsAsync(JObject.FromObject(settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
19
stream_deck_plugin/ClipTrimDotNet/Program.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using BarRaider.SdTools;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace ClipTrimDotNet
|
||||||
|
{
|
||||||
|
internal class Program
|
||||||
|
{
|
||||||
|
static void Main(string[] args)
|
||||||
|
{
|
||||||
|
// Uncomment this line of code to allow for debugging
|
||||||
|
//while (!System.Diagnostics.Debugger.IsAttached) { System.Threading.Thread.Sleep(100); }
|
||||||
|
SDWrapper.Run(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
36
stream_deck_plugin/ClipTrimDotNet/Properties/AssemblyInfo.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
// General Information about an assembly is controlled through the following
|
||||||
|
// set of attributes. Change these attribute values to modify the information
|
||||||
|
// associated with an assembly.
|
||||||
|
[assembly: AssemblyTitle("ClipTrimDotNet")]
|
||||||
|
[assembly: AssemblyDescription("")]
|
||||||
|
[assembly: AssemblyConfiguration("")]
|
||||||
|
[assembly: AssemblyCompany("")]
|
||||||
|
[assembly: AssemblyProduct("ClipTrimDotNet")]
|
||||||
|
[assembly: AssemblyCopyright("Copyright © 2020")]
|
||||||
|
[assembly: AssemblyTrademark("")]
|
||||||
|
[assembly: AssemblyCulture("")]
|
||||||
|
|
||||||
|
// Setting ComVisible to false makes the types in this assembly not visible
|
||||||
|
// to COM components. If you need to access a type in this assembly from
|
||||||
|
// COM, set the ComVisible attribute to true on that type.
|
||||||
|
[assembly: ComVisible(false)]
|
||||||
|
|
||||||
|
// The following GUID is for the ID of the typelib if this project is exposed to COM
|
||||||
|
[assembly: Guid("1bb90885-9d98-46ef-b983-4a4ef3aea890")]
|
||||||
|
|
||||||
|
// Version information for an assembly consists of the following four values:
|
||||||
|
//
|
||||||
|
// Major Version
|
||||||
|
// Minor Version
|
||||||
|
// Build Number
|
||||||
|
// Revision
|
||||||
|
//
|
||||||
|
// You can specify all the values or you can default the Build and Revision Numbers
|
||||||
|
// by using the '*' as shown below:
|
||||||
|
// [assembly: AssemblyVersion("1.0.*")]
|
||||||
|
[assembly: AssemblyVersion("1.0.0.0")]
|
||||||
|
[assembly: AssemblyFileVersion("1.0.0.0")]
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head lang="en">
|
||||||
|
<title>Increment Counter Settings</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<script src="https://sdpi-components.dev/releases/v3/sdpi-components.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!--
|
||||||
|
Learn more about property inspector components at https://sdpi-components.dev/docs/components
|
||||||
|
-->
|
||||||
|
<sdpi-item label="Index">
|
||||||
|
<sdpi-select setting="index" placeholder="file index">
|
||||||
|
<option value="0">0</option>
|
||||||
|
<option value="1">1</option>
|
||||||
|
<option value="2">2</option>
|
||||||
|
<option value="3">3</option>
|
||||||
|
<option value="4">4</option>
|
||||||
|
<option value="5">5</option>
|
||||||
|
<option value="6">6</option>
|
||||||
|
<option value="7">7</option>
|
||||||
|
<option value="8">8</option>
|
||||||
|
<option value="9">9</option>
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="11">11</option>
|
||||||
|
<option value="12">12</option>
|
||||||
|
<option value="13">13</option>
|
||||||
|
<option value="14">14</option>
|
||||||
|
<option value="15">15</option>
|
||||||
|
</sdpi-select>
|
||||||
|
</sdpi-item>
|
||||||
|
<sdpi-item label="Play Mode">
|
||||||
|
<sdpi-select setting="playtype" placeholder="Play Mode">
|
||||||
|
<option value="Play/Overlap">Play/Overlap</option>
|
||||||
|
<option value="Play/Stop">Play/Stop</option>
|
||||||
|
</sdpi-select>
|
||||||
|
</sdpi-item>
|
||||||
|
<sdpi-item label="Volume">
|
||||||
|
<sdpi-range setting="volume" min=".1" max="1" , step="0.05"></sdpi-range>
|
||||||
|
</sdpi-item>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@ -0,0 +1,34 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head lang="en">
|
||||||
|
<title>Increment Counter Settings</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<script src="https://sdpi-components.dev/releases/v3/sdpi-components.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<!--
|
||||||
|
Learn more about property inspector components at https://sdpi-components.dev/docs/components
|
||||||
|
-->
|
||||||
|
<sdpi-item label="Profile Name">
|
||||||
|
<sdpi-select setting="profileName"
|
||||||
|
datasource="getProfiles"
|
||||||
|
show-refresh="true"
|
||||||
|
placeholder="Select a ClipTrim page"></sdpi-select>
|
||||||
|
</sdpi-item>
|
||||||
|
<sdpi-item label="Base Path">
|
||||||
|
<sdpi-file setting="basePath"
|
||||||
|
global="true"
|
||||||
|
webkitdirectory
|
||||||
|
directory
|
||||||
|
multiple></sdpi-file>
|
||||||
|
</sdpi-item>
|
||||||
|
<sdpi-item label="Output Device">
|
||||||
|
<sdpi-select setting="outputDevice"
|
||||||
|
global="true"
|
||||||
|
datasource="getOutputDevices"
|
||||||
|
show-refresh="true"
|
||||||
|
placeholder="Select an Ouput Device"></sdpi-select>
|
||||||
|
</sdpi-item>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
225
stream_deck_plugin/ClipTrimDotNet/WavPlayer.cs
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.ServiceModel.Security;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using BarRaider.SdTools;
|
||||||
|
using NAudio.Wave;
|
||||||
|
using NAudio.Wave.SampleProviders;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
class CachedSound
|
||||||
|
{
|
||||||
|
public byte[] AudioData { get; private set; }
|
||||||
|
public WaveFormat WaveFormat { get; private set; }
|
||||||
|
public CachedSound(string audioFileName)
|
||||||
|
{
|
||||||
|
using (var audioFileReader = new AudioFileReader(audioFileName))
|
||||||
|
{
|
||||||
|
// TODO: could add resampling in here if required
|
||||||
|
WaveFormat = audioFileReader.WaveFormat;
|
||||||
|
var wholeFile = new List<byte>((int)(audioFileReader.Length));
|
||||||
|
var readBuffer = new byte[audioFileReader.WaveFormat.SampleRate * audioFileReader.WaveFormat.Channels*4];
|
||||||
|
int samplesRead;
|
||||||
|
while ((samplesRead = audioFileReader.Read(readBuffer, 0, readBuffer.Length)) > 0)
|
||||||
|
{
|
||||||
|
wholeFile.AddRange(readBuffer.Take(samplesRead));
|
||||||
|
}
|
||||||
|
AudioData = wholeFile.ToArray();
|
||||||
|
}
|
||||||
|
//Logger.Instance.LogMessage(TracingLevel.INFO, $"AudioData Length {AudioData.Length}");
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class CachedSoundSampleProvider : IWaveProvider
|
||||||
|
{
|
||||||
|
private readonly CachedSound cachedSound;
|
||||||
|
private long position;
|
||||||
|
|
||||||
|
~CachedSoundSampleProvider() {
|
||||||
|
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Cache destructor");
|
||||||
|
}
|
||||||
|
|
||||||
|
public CachedSoundSampleProvider(CachedSound cachedSound)
|
||||||
|
{
|
||||||
|
this.cachedSound = cachedSound;
|
||||||
|
position = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Read(byte[] buffer, int offset, int count)
|
||||||
|
{
|
||||||
|
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Read1 byte");
|
||||||
|
var availableSamples = cachedSound.AudioData.Length - position;
|
||||||
|
var samplesToCopy = Math.Min(availableSamples, count);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//Logger.Instance.LogMessage(TracingLevel.INFO, $"{cachedSound.AudioData.GetType()} {buffer.GetType()}");
|
||||||
|
Array.Copy(cachedSound.AudioData, position, buffer, offset, samplesToCopy);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
//Logger.Instance.LogMessage(TracingLevel.INFO, $"{ex.ToString()}");
|
||||||
|
}
|
||||||
|
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Read3");
|
||||||
|
position += samplesToCopy;
|
||||||
|
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Sending {samplesToCopy} samples");
|
||||||
|
return (int)samplesToCopy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public WaveFormat WaveFormat => cachedSound.WaveFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WavPlayer
|
||||||
|
{
|
||||||
|
private static WavPlayer? instance;
|
||||||
|
public static WavPlayer Instance
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
instance ??= new WavPlayer();
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
public enum PlayMode
|
||||||
|
{
|
||||||
|
PlayOverlap,
|
||||||
|
PlayStop
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<string, List<Tuple<WaveOutEvent, IWaveProvider>>> _activePlayers;
|
||||||
|
|
||||||
|
public WavPlayer()
|
||||||
|
{
|
||||||
|
_activePlayers = new ConcurrentDictionary<string, List<Tuple<WaveOutEvent, IWaveProvider>>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Play(string filePath, string id, double volume, PlayMode mode)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath))
|
||||||
|
throw new ArgumentException("File path cannot be null or empty.", nameof(filePath));
|
||||||
|
|
||||||
|
if (mode == PlayMode.PlayOverlap)
|
||||||
|
{
|
||||||
|
PlayWithOverlap(filePath, id, volume);
|
||||||
|
}
|
||||||
|
else if (mode == PlayMode.PlayStop)
|
||||||
|
{
|
||||||
|
PlayWithStop(filePath, id, volume);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(mode), "Invalid play mode specified.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PlayWithOverlap(string filePath, string id, double volume)
|
||||||
|
{
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
//Logger.Instance.LogMessage(TracingLevel.INFO, "Play overlap");
|
||||||
|
var player = CreatePlayer(filePath, id);
|
||||||
|
|
||||||
|
if (!_activePlayers.ContainsKey(filePath))
|
||||||
|
{
|
||||||
|
_activePlayers[filePath] = new List<Tuple<WaveOutEvent, IWaveProvider>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
_activePlayers[filePath].Add(player);
|
||||||
|
player.Item1.Volume = (float)volume;
|
||||||
|
player.Item1.Play();
|
||||||
|
}
|
||||||
|
catch(Exception ex)
|
||||||
|
{
|
||||||
|
//Logger.Instance.LogMessage(TracingLevel.INFO, ex.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
//var playersToDispose = _activePlayers[filePath].Where(x => x.Item1.PlaybackState == PlaybackState.Stopped).ToList();
|
||||||
|
//foreach (var p in playersToDispose)
|
||||||
|
//{
|
||||||
|
// p.Item1.Stop();
|
||||||
|
// p.Item1.Dispose();
|
||||||
|
//}
|
||||||
|
//_activePlayers[filePath].RemoveAll(x => x.Item1.PlaybackState == PlaybackState.Stopped);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PlayWithStop(string filePath, string id, double volume)
|
||||||
|
{
|
||||||
|
if (_activePlayers.TryGetValue(filePath, out var players))
|
||||||
|
{
|
||||||
|
|
||||||
|
// Stop and dispose all current players for this file
|
||||||
|
if (players.Any(x => x.Item1.PlaybackState == PlaybackState.Playing))
|
||||||
|
{
|
||||||
|
var playersToDispose = players.ToList();
|
||||||
|
foreach (var player in playersToDispose)
|
||||||
|
{
|
||||||
|
player.Item1.Stop();
|
||||||
|
player.Item1.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PlayWithOverlap(filePath, id, volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Start a new player
|
||||||
|
PlayWithOverlap(filePath, id, volume);
|
||||||
|
}
|
||||||
|
|
||||||
|
_activePlayers[filePath].RemoveAll(x => x.Item1.PlaybackState == PlaybackState.Stopped);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Tuple<WaveOutEvent, IWaveProvider> CreatePlayer(string filePath, string name)
|
||||||
|
{
|
||||||
|
var reader = new CachedSoundSampleProvider(new CachedSound(filePath));
|
||||||
|
//var reader = new AudioFileReader(filePath);
|
||||||
|
int number = -1;
|
||||||
|
for (int i = 0; i < WaveOut.DeviceCount; ++i)
|
||||||
|
{
|
||||||
|
if (WaveOut.GetCapabilities(i).ProductName == name)
|
||||||
|
{
|
||||||
|
number = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var player = new WaveOutEvent() { DeviceNumber = number };
|
||||||
|
player.Init(reader);
|
||||||
|
return new Tuple<WaveOutEvent, IWaveProvider>(player, reader);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CleanupPlayer(string filePath)
|
||||||
|
{
|
||||||
|
if (_activePlayers.TryGetValue(filePath, out var players))
|
||||||
|
{
|
||||||
|
var playersToDispose = players.ToList();
|
||||||
|
foreach (var p in playersToDispose)
|
||||||
|
{
|
||||||
|
p.Item1.Stop();
|
||||||
|
p.Item1.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_activePlayers[filePath].RemoveAll(x => x.Item1.PlaybackState == PlaybackState.Stopped);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopAll()
|
||||||
|
{
|
||||||
|
foreach (var players in _activePlayers.Values)
|
||||||
|
{
|
||||||
|
var playersToDispose = players.ToList();
|
||||||
|
foreach (var player in playersToDispose)
|
||||||
|
{
|
||||||
|
player.Item1.Stop();
|
||||||
|
player.Item1.Dispose();
|
||||||
|
}
|
||||||
|
players.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
_activePlayers.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
62
stream_deck_plugin/ClipTrimDotNet/manifest.json
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
|
||||||
|
"Actions": [
|
||||||
|
{
|
||||||
|
"Icon": "Images/icon",
|
||||||
|
"Name": "Player",
|
||||||
|
"States": [
|
||||||
|
{
|
||||||
|
"Image": "Images/pluginAction",
|
||||||
|
"TitleAlignment": "middle",
|
||||||
|
"FontSize": 11
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"SupportedInMultiActions": false,
|
||||||
|
"Tooltip": "Plays a bound audio file",
|
||||||
|
"UUID": "com.michal-courson.cliptrim.player",
|
||||||
|
"PropertyInspectorPath": "PropertyInspector/file_player.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Icon": "Images/icon",
|
||||||
|
"Name": "Profile Switcher",
|
||||||
|
"States": [
|
||||||
|
{
|
||||||
|
"Image": "Images/pluginAction",
|
||||||
|
"TitleAlignment": "middle",
|
||||||
|
"FontSize": 11
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"SupportedInMultiActions": false,
|
||||||
|
"Tooltip": "Selects which sub folder to use and opens effect profile",
|
||||||
|
"UUID": "com.michal-courson.cliptrim.profile-switcher",
|
||||||
|
"PropertyInspectorPath": "PropertyInspector/profile_swticher.html"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Author": "Michal Courson",
|
||||||
|
"Name": "ClipTrimDotNet",
|
||||||
|
"Description": "Pairs with cliptrim for easy voice recording and trimming",
|
||||||
|
"Icon": "Images/pluginIcon",
|
||||||
|
"Version": "0.1.0.0",
|
||||||
|
"CodePath": "ClipTrimDotNet.exe",
|
||||||
|
"Category": "ClipTrimDotNet",
|
||||||
|
"CategoryIcon": "Images/categoryIcon",
|
||||||
|
"UUID": "com.michal-courson.cliptrim",
|
||||||
|
"OS": [
|
||||||
|
{
|
||||||
|
"Platform": "windows",
|
||||||
|
"MinimumVersion": "10"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"SDKVersion": 2,
|
||||||
|
"Software": {
|
||||||
|
"MinimumVersion": "6.4"
|
||||||
|
},
|
||||||
|
"Profiles": [
|
||||||
|
{
|
||||||
|
"Name": "ClipTrim",
|
||||||
|
"DeviceType": 0,
|
||||||
|
"Readonly": false,
|
||||||
|
"DontAutoSwitchWhenInstalled": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
1
stream_deck_plugin/ClipTrimDotNet/pack.bat
Normal file
@ -0,0 +1 @@
|
|||||||
|
npm exec streamdeck restart com.michal-courson.cliptrim
|
||||||
272
stream_deck_plugin/ClipTrimDotNet/package-lock.json
generated
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
{
|
||||||
|
"name": "ClipTrimDotNet",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"devDependencies": {
|
||||||
|
"shx": "^0.3.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/balanced-match": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/brace-expansion": {
|
||||||
|
"version": "1.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
|
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": "^1.0.0",
|
||||||
|
"concat-map": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/concat-map": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/fs.realpath": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/glob": {
|
||||||
|
"version": "7.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||||
|
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
|
||||||
|
"deprecated": "Glob versions prior to v9 are no longer supported",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"fs.realpath": "^1.0.0",
|
||||||
|
"inflight": "^1.0.4",
|
||||||
|
"inherits": "2",
|
||||||
|
"minimatch": "^3.1.1",
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"path-is-absolute": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/inflight": {
|
||||||
|
"version": "1.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||||
|
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
|
||||||
|
"deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"once": "^1.3.0",
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/interpret": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-core-module": {
|
||||||
|
"version": "2.16.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
|
||||||
|
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minimatch": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^1.1.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minimist": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/once": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-is-absolute": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-parse": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/rechoir": {
|
||||||
|
"version": "0.6.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
|
||||||
|
"integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"resolve": "^1.1.6"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/resolve": {
|
||||||
|
"version": "1.22.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||||
|
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"is-core-module": "^2.16.0",
|
||||||
|
"path-parse": "^1.0.7",
|
||||||
|
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"resolve": "bin/resolve"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/shelljs": {
|
||||||
|
"version": "0.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz",
|
||||||
|
"integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"glob": "^7.0.0",
|
||||||
|
"interpret": "^1.0.0",
|
||||||
|
"rechoir": "^0.6.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"shjs": "bin/shjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/shx": {
|
||||||
|
"version": "0.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz",
|
||||||
|
"integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"minimist": "^1.2.3",
|
||||||
|
"shelljs": "^0.8.5"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"shx": "lib/cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/supports-preserve-symlinks-flag": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrappy": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
stream_deck_plugin/ClipTrimDotNet/package.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"stop": "streamdeck stop com.michal-courson.cliptrim",
|
||||||
|
"copy": "@powershell robocopy bin/Debug/ClipTrimDotNet.sdPlugin bin/Debug/com.michal-courson.cliptrim.sdPlugin",
|
||||||
|
"link": "streamdeck link bin/Debug/com.michal-courson.cliptrim.sdPlugin",
|
||||||
|
"restart": "streamdeck restart com.michal-courson.cliptrim",
|
||||||
|
"start": "npm run link && npm run restart",
|
||||||
|
"all": "npm run stop && npm run copy && npm run link && npm run restart",
|
||||||
|
"pack": "streamdeck pack bin/Debug/com.michal-courson.cliptrim.sdPlugin/"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"shx": "^0.3.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
18
stream_deck_plugin/ClipTrimDotNet/packages.config
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<packages>
|
||||||
|
<package id="CommandLineParser" version="2.9.1" targetFramework="net472" />
|
||||||
|
<package id="Microsoft.Win32.Registry" version="4.7.0" targetFramework="net48" />
|
||||||
|
<package id="NAudio" version="2.2.1" targetFramework="net48" />
|
||||||
|
<package id="NAudio.Asio" version="2.2.1" targetFramework="net48" />
|
||||||
|
<package id="NAudio.Core" version="2.2.1" targetFramework="net48" />
|
||||||
|
<package id="NAudio.Midi" version="2.2.1" targetFramework="net48" />
|
||||||
|
<package id="NAudio.Wasapi" version="2.2.1" targetFramework="net48" />
|
||||||
|
<package id="NAudio.WinForms" version="2.2.1" targetFramework="net48" />
|
||||||
|
<package id="NAudio.WinMM" version="2.2.1" targetFramework="net48" />
|
||||||
|
<package id="Newtonsoft.Json" version="13.0.4" targetFramework="net48" />
|
||||||
|
<package id="NLog" version="6.0.5" targetFramework="net48" />
|
||||||
|
<package id="StreamDeck-Tools" version="6.3.2" targetFramework="net48" />
|
||||||
|
<package id="System.Drawing.Common" version="9.0.10" targetFramework="net48" />
|
||||||
|
<package id="System.Security.AccessControl" version="4.7.0" targetFramework="net48" />
|
||||||
|
<package id="System.Security.Principal.Windows" version="4.7.0" targetFramework="net48" />
|
||||||
|
</packages>
|
||||||