Merge branch 'socket' into react_migration

This commit is contained in:
michalcourson
2026-02-28 17:04:44 -05:00
80 changed files with 2471 additions and 2301 deletions

70
.github/upgrades/dotnet-upgrade-plan.md vendored Normal file
View File

@ -0,0 +1,70 @@
# .NET 8.0 Upgrade Plan
## Execution Steps
Execute steps below sequentially one by one in the order they are listed.
1. Validate that an .NET 8.0 SDK required for this upgrade is installed on the machine and if not, help to get it installed.
2. Ensure that the SDK version specified in global.json files is compatible with the .NET 8.0 upgrade.
3. Upgrade ClipTrimDotNet\ClipTrimDotNet.csproj
4. Run unit tests to validate upgrade in the projects listed below:
- ClientTest\ClientTest.csproj
## Settings
This section contains settings and data used by execution steps.
### Excluded projects
| Project name | Description |
|:-----------------------------------------------|:---------------------------:|
### Aggregate NuGet packages modifications across all projects
NuGet packages used across all selected projects or their dependencies that need version update in projects that reference them.
| Package Name | Current Version | New Version | Description |
|:------------------------------------|:---------------:|:-----------:|:----------------------------------------------|
| Microsoft.Bcl.AsyncInterfaces | 10.0.2 | 8.0.0 | Recommended for .NET 8.0 |
| Microsoft.Extensions.DependencyInjection | 10.0.2 | 8.0.1 | Recommended for .NET 8.0 |
| Microsoft.Extensions.DependencyInjection.Abstractions | 10.0.2 | 8.0.2 | Recommended for .NET 8.0 |
| Microsoft.Extensions.Logging | 10.0.2 | 8.0.1 | Recommended for .NET 8.0 |
| Microsoft.Extensions.Logging.Abstractions | 10.0.2 | 8.0.3 | Recommended for .NET 8.0 |
| Microsoft.Extensions.Options | 10.0.2 | 8.0.2 | Recommended for .NET 8.0 |
| Microsoft.Extensions.Primitives | 10.0.2 | 8.0.0 | Recommended for .NET 8.0 |
| System.Diagnostics.DiagnosticSource | 10.0.2 | 8.0.1 | Recommended for .NET 8.0 |
| System.Drawing.Common | 9.0.10 | 8.0.24 | Recommended for .NET 8.0 |
| System.IO.Pipelines | 10.0.2 | 8.0.0 | Recommended for .NET 8.0 |
| System.Security.AccessControl | 4.7.0 | 6.0.1 | Recommended for .NET 8.0 |
| System.Text.Encodings.Web | 10.0.2 | 8.0.0 | Recommended for .NET 8.0 |
| System.Text.Json | 10.0.2 | 8.0.6 | Recommended for .NET 8.0 |
| Microsoft.Win32.Registry | 4.7.0 | | Functionality included with framework |
| System.Buffers | 4.6.1 | | Functionality included with framework |
| System.IO | 4.3.0 | | Functionality included with framework |
| System.Memory | 4.6.3 | | Functionality included with framework |
| System.Net.Http | 4.3.4 | | Functionality included with framework |
| System.Numerics.Vectors | 4.6.1 | | Functionality included with framework |
| System.Runtime | 4.3.0 | | Functionality included with framework |
| System.Security.Cryptography.Algorithms | 4.3.0 | | Functionality included with framework |
| System.Security.Cryptography.Encoding | 4.3.0 | | Functionality included with framework |
| System.Security.Cryptography.Primitives | 4.3.0 | | Functionality included with framework |
| System.Security.Cryptography.X509Certificates | 4.3.0 | | Functionality included with framework |
| System.Security.Principal.Windows | 4.7.0 | | Functionality included with framework |
| System.Threading.Tasks.Extensions | 4.6.3 | | Functionality included with framework |
| System.ValueTuple | 4.6.1 | | Functionality included with framework |
### Project upgrade details
#### ClipTrimDotNet\ClipTrimDotNet.csproj modifications
Project properties changes:
- Target framework should be changed from `.NETFramework,Version=v4.8` to `net8.0`
- Project file should be converted to SDK-style
NuGet packages changes:
- Update all packages listed in the NuGet packages table above as recommended
- Remove packages whose functionality is now included with the framework
Other changes:
- Ensure compatibility with .NET 8.0 APIs and features
- Update code as needed for breaking changes

View File

@ -1,3 +1,3 @@
{ {
"idf.pythonInstallPath": "C:\\Users\\mickl\\.espressif\\tools\\idf-python\\3.11.2\\python.exe" "idf.pythonInstallPath": "C:\\Users\\mickl\\.espressif\\tools\\idf-python\\3.11.2\\python.exe"
} }

View File

@ -1,34 +1,134 @@
[ [
{ {
"name": "Uncategorized", "name": "Uncategorized",
"id": 0, "id": 0,
"clips": [] "clips": [
}, {
{ "filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260226_195932.wav",
"name": "Test", "name": "Clip 20260226_195932",
"id": 1, "playbackType": "playOverlap",
"clips": [] "volume": 1
}, },
{ {
"name": "New", "filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260228_165611.wav",
"id": 2, "name": "Clip 20260228_165611",
"clips": [ "playbackType": "playStop",
{ "volume": 1.0
"endTime": 30, },
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260220_193822.wav", {
"name": "Pee pee\npoo poo", "filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260228_165646.wav",
"playbackType": "playOverlap", "name": "Clip 20260228_165646",
"startTime": 27.76674010920584, "playbackType": "playStop",
"volume": 0.25 "volume": 1.0
}, }
{ ]
"endTime": 27.516843118383072, },
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260220_200442.wav", {
"name": "Clip 20260220_200442", "name": "Test",
"playbackType": "playOverlap", "id": 1,
"startTime": 25.120307988450435, "clips": [
"volume": 0.64 {
} "filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260226_183812.wav",
] "name": "Clip 20260226_183812",
} "playbackType": "playStop",
"volume": 1
},
{
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260226_183607.wav",
"name": "Clip 20260226_183607",
"playbackType": "playStop",
"volume": 1
},
{
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260226_183822.wav",
"name": "Clip 20260226_183822",
"playbackType": "playStop",
"volume": 1
},
{
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260226_184028.wav",
"name": "Clip 20260226_184028",
"playbackType": "playStop",
"volume": 1
},
{
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260226_184030.wav",
"name": "Clip 20260226_184030",
"playbackType": "playStop",
"volume": 1
},
{
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260226_184032.wav",
"name": "Clip 20260226_184032",
"playbackType": "playStop",
"volume": 1
},
{
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260226_184037.wav",
"name": "Clip 20260226_184037",
"playbackType": "playStop",
"volume": 1
},
{
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260226_184040.wav",
"name": "Clip 20260226_184040",
"playbackType": "playStop",
"volume": 1
},
{
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260226_184041.wav",
"name": "Clip 20260226_184041",
"playbackType": "playStop",
"volume": 1
},
{
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260226_184042.wav",
"name": "Clip 20260226_184042",
"playbackType": "playStop",
"volume": 1
},
{
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260228_092721.wav",
"name": "Clip 20260228_092721",
"playbackType": "playStop",
"volume": 1,
"startTime": 6.438382145377559,
"endTime": 14.277258292166426
}
]
},
{
"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": "playOverlap",
"startTime": 27.64044943820222,
"volume": 0.31
},
{
"endTime": 30,
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260220_200442.wav",
"name": "Test",
"playbackType": "playOverlap",
"startTime": 26.14685314685314,
"volume": 0.64
},
{
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260228_085116.wav",
"name": "pp",
"playbackType": "playStop",
"volume": 1
},
{
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260228_120955.wav",
"name": "nose",
"playbackType": "playStop",
"volume": 1
}
]
}
] ]

View File

@ -1,7 +1,6 @@
sounddevice==0.5.1 Flask==3.1.3
numpy==1.22.3 flask_cors==6.0.2
python-osc==1.9.3 flask_socketio==5.6.1
scipy==1.10.1 numpy==2.4.2
comtypes==1.4.8 scipy==1.17.1
pycaw==20240210 sounddevice==0.5.5
Flask==3.1.2

View File

@ -1,17 +1,17 @@
{ {
"input_device": { "input_device": {
"channels": 2, "channels": 2,
"default_samplerate": 48000, "default_samplerate": 48000,
"index": 55, "index": 55,
"name": "VM Mic mix (VB-Audio Voicemeeter VAIO)" "name": "VM Mic mix (VB-Audio Voicemeeter VAIO)"
}, },
"save_path": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings", "save_path": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings",
"recording_length": 30, "recording_length": 30,
"output_device": { "output_device": {
"channels": 2, "channels": 2,
"default_samplerate": 48000, "default_samplerate": 48000,
"index": 45, "index": 45,
"name": "VM to Discord (VB-Audio Voicemeeter VAIO)" "name": "VM to Discord (VB-Audio Voicemeeter VAIO)"
}, },
"http_port": 5010 "http_port": 5010
} }

View File

@ -1,166 +1,168 @@
import sounddevice as sd import sounddevice as sd
import numpy as np 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 from metadata_manager import MetaDataManager
from audio_clip import AudioClip from audio_clip import AudioClip
# AudioClip class for clip playback # AudioClip class for clip playback
class AudioIO: class AudioIO:
_instance = None _instance = None
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
if cls._instance is None: if cls._instance is None:
# print("Creating new AudioRecorder instance") # print("Creating new AudioRecorder instance")
cls._instance = super().__new__(cls) cls._instance = super().__new__(cls)
cls._instance.init() cls._instance.init()
return cls._instance return cls._instance
def init(self): def init(self):
self.duration = 30 self.duration = 30
self.channels = 2 self.channels = 2
self.input_sample_rate = 44100 self.input_sample_rate = 44100
self.output_sample_rate = 44100 self.output_sample_rate = 44100
self.buffer = np.zeros((int(self.duration * self.input_sample_rate), self.channels), dtype=np.float32) self.buffer = np.zeros((int(self.duration * self.input_sample_rate), self.channels), dtype=np.float32)
self.recordings_dir = "recordings" self.recordings_dir = "recordings"
sd.default.latency = 'low' sd.default.latency = 'low'
self.in_stream = sd.InputStream( self.socket = None
callback=self.record_callback
) self.in_stream = sd.InputStream(
callback=self.record_callback
self.out_stream = sd.OutputStream( )
callback=self.playback_callback,
latency=3 self.out_stream = sd.OutputStream(
) callback=self.playback_callback,
latency=3
self.clip_map = {} )
self.clip_map = {}
def refresh_streams(self):
was_active = self.in_stream.active
if was_active: def refresh_streams(self):
self.in_stream.stop() was_active = self.in_stream.active
self.out_stream.stop() if was_active:
self.in_stream.stop()
self.buffer = np.zeros((int(self.duration * self.input_sample_rate), self.channels), dtype=np.float32) self.out_stream.stop()
# print(f"AudioRecorder initialized with duration={self.duration}s, sample_rate={self.sample_rate}Hz, channels={self.channels}")
self.in_stream = sd.InputStream( self.buffer = np.zeros((int(self.duration * self.input_sample_rate), self.channels), dtype=np.float32)
callback=self.record_callback # print(f"AudioRecorder initialized with duration={self.duration}s, sample_rate={self.sample_rate}Hz, channels={self.channels}")
) self.in_stream = sd.InputStream(
callback=self.record_callback
self.out_stream = sd.OutputStream( )
callback=self.playback_callback
) self.out_stream = sd.OutputStream(
callback=self.playback_callback
if was_active: )
self.in_stream.start()
self.out_stream.start() if was_active:
self.in_stream.start()
self.out_stream.start()
def record_callback(self, indata, frames, time, status):
if status:
# print(f"Recording status: {status}") def record_callback(self, indata, frames, time, status):
pass if status:
# print(f"Recording status: {status}")
# Circular buffer implementation pass
self.buffer = np.roll(self.buffer, -frames, axis=0)
self.buffer[-frames:] = indata # Circular buffer implementation
self.buffer = np.roll(self.buffer, -frames, axis=0)
def playback_callback(self, outdata, frames, time, status): self.buffer[-frames:] = indata
if status:
# print(f"Playback status: {status}") def playback_callback(self, outdata, frames, time, status):
pass if status:
# print(f"Playback status: {status}")
outdata.fill(0) pass
# Iterate over a copy of the items to avoid modifying the dictionary during iteration outdata.fill(0)
for clip_id, clip_list in list(self.clip_map.items()):
for clip in clip_list[:]: # Iterate over a copy of the list # Iterate over a copy of the items to avoid modifying the dictionary during iteration
if not clip.is_finished(): for clip_id, clip_list in list(self.clip_map.items()):
samples = clip.get_samples(frames) for clip in clip_list[:]: # Iterate over a copy of the list
outdata[:] += samples.reshape(-1, 1) # Mix into output if not clip.is_finished():
if clip.is_finished(): samples = clip.get_samples(frames)
self.clip_map[clip_id].remove(clip) outdata[:] += samples.reshape(-1, 1) # Mix into output
if len(self.clip_map[clip_id]) == 0: if clip.is_finished():
del self.clip_map[clip_id] self.clip_map[clip_id].remove(clip)
break # Exit inner loop since the key is deleted if len(self.clip_map[clip_id]) == 0:
del self.clip_map[clip_id]
break # Exit inner loop since the key is deleted
def save_last_n_seconds(self):
# Create output directory if it doesn't exist
os.makedirs(self.recordings_dir, exist_ok=True) def save_last_n_seconds(self):
# Create output directory if it doesn't exist
# Generate filename with timestamp os.makedirs(self.recordings_dir, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = os.path.join(self.recordings_dir, f"audio_capture_{timestamp}.wav") # Generate filename with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# Normalize audio to prevent clipping filename = os.path.join(self.recordings_dir, f"audio_capture_{timestamp}.wav")
audio_data = self.buffer / np.max(np.abs(self.buffer)) * .5
# Normalize audio to prevent clipping
# Convert float32 to int16 for WAV file audio_data = self.buffer / np.max(np.abs(self.buffer)) * .5
audio_data_int16 = (audio_data * 32767).astype(np.int16)
# Convert float32 to int16 for WAV file
# Write buffer to file audio_data_int16 = (audio_data * 32767).astype(np.int16)
wavfile.write(filename, int(self.input_sample_rate), audio_data_int16)
# Write buffer to file
meta = MetaDataManager() wavfile.write(filename, int(self.input_sample_rate), audio_data_int16)
clip_metadata = { meta = MetaDataManager()
"filename": filename,
"name": f"Clip {timestamp}", clip_metadata = {
"playbackType":"playStop", "filename": filename,
"volume": 1.0, "name": f"Clip {timestamp}",
} "playbackType":"playStop",
"volume": 1.0,
meta.add_clip_to_collection("Uncategorized", clip_metadata ) }
meta.add_clip_to_collection("Uncategorized", clip_metadata )
return clip_metadata self.socket.emit('new_clip', clip_metadata)
def set_buffer_duration(self, duration): return clip_metadata
self.duration = duration
self.buffer = np.zeros((int(duration * self.input_sample_rate), self.channels), dtype=np.float32) def set_buffer_duration(self, duration):
self.duration = duration
def set_recording_directory(self, directory): self.buffer = np.zeros((int(duration * self.input_sample_rate), self.channels), dtype=np.float32)
self.recordings_dir = directory
def set_recording_directory(self, directory):
def start_recording(self): self.recordings_dir = directory
if(self.in_stream.active):
# print("Already recording") def start_recording(self):
return if(self.in_stream.active):
# print('number of channels', self.channels) # print("Already recording")
return
self.in_stream.start() # print('number of channels', self.channels)
self.out_stream.start()
self.output_sample_rate = self.out_stream.samplerate self.in_stream.start()
self.input_sample_rate = self.in_stream.samplerate self.out_stream.start()
self.output_sample_rate = self.out_stream.samplerate
def stop_recording(self): self.input_sample_rate = self.in_stream.samplerate
if(not self.in_stream.active):
# print("Already stopped") def stop_recording(self):
return if(not self.in_stream.active):
# print("Already stopped")
self.in_stream.stop() return
self.out_stream.stop()
self.in_stream.stop()
def is_recording(self): self.out_stream.stop()
return self.in_stream.active
def is_recording(self):
def play_clip(self, clip_metadata): return self.in_stream.active
print(f"Playing clip: {clip_metadata}")
clip_id = clip_metadata.get("filename") def play_clip(self, clip_metadata):
if clip_metadata.get("playbackType") == "playStop": print(f"Playing clip: {clip_metadata}")
if clip_id in self.clip_map: clip_id = clip_metadata.get("filename")
del self.clip_map[clip_id] if clip_metadata.get("playbackType") == "playStop":
return if clip_id in self.clip_map:
else: del self.clip_map[clip_id]
self.clip_map[clip_id] = [] return
if clip_id not in self.clip_map: else:
self.clip_map[clip_id] = [] self.clip_map[clip_id] = []
if clip_id not in self.clip_map:
self.clip_map[clip_id] = []
self.clip_map[clip_id].append(AudioClip(clip_metadata, target_sample_rate=self.output_sample_rate)) self.clip_map[clip_id].append(AudioClip(clip_metadata, target_sample_rate=self.output_sample_rate))

View File

@ -1,68 +1,94 @@
import argparse import argparse
import os import os
import sys import sys
from audio_io import AudioIO from audio_io import AudioIO
from windows_audio import WindowsAudioManager from windows_audio import WindowsAudioManager
import sounddevice as sd import sounddevice as sd
from metadata_manager import MetaDataManager from metadata_manager import MetaDataManager
from settings import SettingsManager from settings import SettingsManager
from flask import Flask from flask import Flask
from flask_cors import CORS from flask_cors import CORS
from routes.recording import recording_bp from routes.recording import recording_bp
from routes.device import device_bp from routes.device import device_bp
from routes.metadata import metadata_bp from routes.metadata import metadata_bp
from routes.settings import settings_bp from routes.settings import settings_bp
from flask_socketio import SocketIO from flask_socketio import SocketIO, emit
import threading import threading
app = Flask(__name__) app = Flask(__name__)
CORS(app) CORS(app)
# socketio = SocketIO(app, cors_allowed_origins="*") socketio = SocketIO(app, cors_allowed_origins="*", logger=True, engineio_logger=True, async_mode='eventlet')
# CORS(socketio)
@socketio.on('connect')
def main(): def handle_connect():
# Create argument parser print("Client connected")
parser = argparse.ArgumentParser(description='Audio Recording Service') emit('full_data', MetaDataManager().collections)
# OSC port argument @socketio.on('record_clip')
parser.add_argument('--osc-port', def record_clip():
type=int, io = AudioIO()
help='OSC server port number', io.save_last_n_seconds();
default=5010)
@socketio.on('play_clip')
# Parse arguments def play_clip(data):
args = parser.parse_args() io = AudioIO()
audio_manager = WindowsAudioManager() print(f"Received play_clip event with data: {data}")
settings = SettingsManager() if data:
io.play_clip(data)
# Ensure save path exists
os.makedirs(settings.get_settings('save_path'), exist_ok=True)
def main():
# Create argument parser
io = AudioIO() parser = argparse.ArgumentParser(description='Audio Recording Service')
io.start_recording()
# Register blueprints # OSC port argument
app.register_blueprint(recording_bp) parser.add_argument('--osc-port',
app.register_blueprint(device_bp) type=int,
app.register_blueprint(metadata_bp) help='OSC server port number',
app.register_blueprint(settings_bp) default=5010)
app.run(host='127.0.0.1', port=settings.get_settings('http_port'), debug=False, use_reloader=True)
# socketio.run(app, host='127.0.0.1', port=args.osc_port, debug=False, use_reloader=True) # Parse arguments
args = parser.parse_args()
audio_manager = WindowsAudioManager()
settings = SettingsManager()
# Run the OSC server meta = MetaDataManager()
# try:
# print(f"Starting OSC Recording Server on port {args.osc_port}")
# # osc_server.run_server()
# except KeyboardInterrupt: # Ensure save path exists
# print("\nServer stopped by user.") os.makedirs(settings.get_settings('save_path'), exist_ok=True)
# except Exception as e:
# print(f"Error starting server: {e}")
# sys.exit(1) io = AudioIO()
io.start_recording()
if __name__ == "__main__": # settings.socket = socketio
io.socket = socketio
meta.socket = socketio
# Register blueprints
app.register_blueprint(recording_bp)
app.register_blueprint(device_bp)
app.register_blueprint(metadata_bp)
app.register_blueprint(settings_bp)
print(f"Starting Flask server on port {settings.get_settings('http_port')}")
# app.run(host='127.0.0.1', port=settings.get_settings('http_port'), debug=False, use_reloader=True)
socketio.run(app, host='127.0.0.1', port=settings.get_settings('http_port'), debug=False, use_reloader=True, allow_unsafe_werkzeug=True)
# Run the OSC server
# try:
# print(f"Starting OSC Recording Server on port {args.osc_port}")
# # osc_server.run_server()
# except KeyboardInterrupt:
# print("\nServer stopped by user.")
# except Exception as e:
# print(f"Error starting server: {e}")
# sys.exit(1)
if __name__ == "__main__":
main() main()

View File

@ -1,20 +1,20 @@
{ {
"Uncategorized": [ "Uncategorized": [
{ {
"endTime": 12.489270386266055, "endTime": 12.489270386266055,
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\src\\recordings\\audio_capture_20260214_133540.wav", "filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\src\\recordings\\audio_capture_20260214_133540.wav",
"name": "Clip 20260214_133540", "name": "Clip 20260214_133540",
"playbackType": "playStop", "playbackType": "playStop",
"startTime": 10.622317596566523, "startTime": 10.622317596566523,
"volume": 1 "volume": 1
}, },
{ {
"endTime": 6.824034334763957, "endTime": 6.824034334763957,
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\src\\recordings\\audio_capture_20260214_133137.wav", "filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\src\\recordings\\audio_capture_20260214_133137.wav",
"name": "Clip 20260214_133137", "name": "Clip 20260214_133137",
"playbackType": "playStop", "playbackType": "playStop",
"startTime": 3.7982832618025753, "startTime": 3.7982832618025753,
"volume": 1 "volume": 1
} }
] ]
} }

View File

@ -1,100 +1,114 @@
import os import os
import json import json
class MetaDataManager: class MetaDataManager:
_instance = None _instance = None
def __new__(cls, *args, **kwargs): def __new__(cls, *args, **kwargs):
if cls._instance is None: if cls._instance is None:
cls._instance = super().__new__(cls) cls._instance = super().__new__(cls)
cls._instance.init() cls._instance.init()
return cls._instance return cls._instance
def init(self): def init(self):
# read metadata file from executing directory self.socket = None
self.metadata_file = os.path.join(os.getcwd(), "metadata.json") # read metadata file from executing directory
if os.path.exists(self.metadata_file): self.metadata_file = os.path.join(os.getcwd(), "metadata.json")
with open(self.metadata_file, "r") as f: if os.path.exists(self.metadata_file):
self.collections = json.load(f) with open(self.metadata_file, "r") as f:
else: self.collections = json.load(f)
self.collections = {} else:
if(collections := next((c for c in self.collections if c.get("name") == "Uncategorized"), None)) is None: self.collections = {}
self.collections.append({"name": "Uncategorized", "id": 0, "clips": []}) if(collections := next((c for c in self.collections if c.get("name") == "Uncategorized"), None)) is None:
self.save_metadata() 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): def create_collection(self, name):
raise ValueError(f"Collection '{name}' already exists.") if any(c.get("name") == name for c in self.collections):
new_id = max((c.get("id", 0) for c in self.collections), default=0) + 1 raise ValueError(f"Collection '{name}' already exists.")
self.collections.append({"name": name, "id": new_id, "clips": []}) new_id = max((c.get("id", 0) for c in self.collections), default=0) + 1
self.save_metadata() 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) def delete_collection(self, name):
if collection is None: collection = next((c for c in self.collections if c.get("name") == name), None)
raise ValueError(f"Collection '{name}' does not exist.") if collection is None:
self.collections.remove(collection) raise ValueError(f"Collection '{name}' does not exist.")
self.save_metadata() 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) def add_clip_to_collection(self, collection_name, clip_metadata):
if collection is None: collection = next((c for c in self.collections if c.get("name") == collection_name), None)
raise ValueError(f"Collection '{collection_name}' does not exist.") if collection is None:
collection["clips"].append(clip_metadata) raise ValueError(f"Collection '{collection_name}' does not exist.")
self.save_metadata() collection["clips"].append(clip_metadata)
if not self.socket is None:
def remove_clip_from_collection(self, collection_name, clip_metadata): self.socket.emit('collection_updated', collection)
collection = next((c for c in self.collections if c.get("name") == collection_name), None) self.save_metadata()
if collection is None:
raise ValueError(f"Collection '{collection_name}' does not exist.") def remove_clip_from_collection(self, collection_name, clip_metadata):
# Remove all clips with the same file name as clip_metadata["file_name"] collection = next((c for c in self.collections if c.get("name") == collection_name), None)
in_list = any(clip.get("filename") == clip_metadata.get("filename") for clip in collection["clips"]) if collection is None:
if not in_list: raise ValueError(f"Collection '{collection_name}' does not exist.")
raise ValueError(f"Clip with filename '{clip_metadata.get('filename')}' not found in collection '{collection_name}'.") # 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"])
collection["clips"] = [ if not in_list:
clip for clip in collection["clips"] raise ValueError(f"Clip with filename '{clip_metadata.get('filename')}' not found in collection '{collection_name}'.")
if clip.get("filename") != clip_metadata.get("filename")
] collection["clips"] = [
self.save_metadata() clip for clip in collection["clips"]
if clip.get("filename") != clip_metadata.get("filename")
def move_clip_to_collection(self, source_collection, target_collection, clip_metadata): ]
self.remove_clip_from_collection(source_collection, clip_metadata) if not self.socket is None:
self.add_clip_to_collection(target_collection, clip_metadata) self.socket.emit('collection_updated', collection)
self.save_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: def move_clip_to_collection(self, source_collection, target_collection, clip_metadata):
raise ValueError(f"Collection '{collection_name}' does not exist.") self.remove_clip_from_collection(source_collection, clip_metadata)
# Find the index of the clip with the same file name as old_clip_metadata["file_name"] self.add_clip_to_collection(target_collection, clip_metadata)
index = next((i for i, clip in enumerate(collection["clips"]) if clip.get("filename") == new_clip_metadata.get("filename")), None) if not self.socket is None:
if index is None: self.socket.emit('collection_updated', source_collection)
raise ValueError(f"Clip with filename '{new_clip_metadata.get('filename')}' not found in collection '{collection_name}'.") self.socket.emit('collection_updated', target_collection)
collection["clips"][index] = new_clip_metadata
self.save_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)
def get_collections(self): if collection is None:
return list(map(lambda c: {"name": c.get("name"), "id": c.get("id")}, self.collections)) 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"]
def get_clips_in_collection(self, collection_name): index = next((i for i, clip in enumerate(collection["clips"]) if clip.get("filename") == new_clip_metadata.get("filename")), None)
collection = next((c for c in self.collections if c.get("name") == collection_name), None) if index is None:
if collection is None: raise ValueError(f"Clip with filename '{new_clip_metadata.get('filename')}' not found in collection '{collection_name}'.")
raise ValueError(f"Collection '{collection_name}' does not exist.")
return collection["clips"] collection["clips"][index] = new_clip_metadata
if not self.socket is None:
def reorder_clips_in_collection(self, collection_name, new_order): self.socket.emit('collection_updated', collection)
collection = next((c for c in self.collections if c.get("name") == collection_name), None) self.save_metadata()
if collection is None:
raise ValueError(f"Collection '{collection_name}' does not exist.") def get_collections(self):
existing_filenames = {clip.get("filename") for clip in collection["clips"]} return list(map(lambda c: {"name": c.get("name"), "id": c.get("id")}, self.collections))
new_filenames = {clip.get("filename") for clip in new_order}
def get_clips_in_collection(self, collection_name):
if not new_filenames.issubset(existing_filenames): collection = next((c for c in self.collections if c.get("name") == collection_name), None)
raise ValueError("New order contains clips that do not exist in the collection.") if collection is None:
raise ValueError(f"Collection '{collection_name}' does not exist.")
collection["clips"] = new_order return collection["clips"]
self.save_metadata()
def reorder_clips_in_collection(self, collection_name, new_order):
def save_metadata(self): collection = next((c for c in self.collections if c.get("name") == collection_name), None)
with open(self.metadata_file, "w") as f: if collection is None:
json.dump(self.collections, f, indent=4) 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
if not self.socket is None:
self.socket.emit('collection_updated', collection)
self.save_metadata()
def save_metadata(self):
with open(self.metadata_file, "w") as f:
json.dump(self.collections, f, indent=4)

View File

@ -1,4 +1,4 @@
[ViewState] [ViewState]
Mode= Mode=
Vid= Vid=
FolderType=Generic FolderType=Generic

View File

@ -98,3 +98,9 @@ def edit_clip_in_collection():
return jsonify({'status': 'success', 'collections': collections}) return jsonify({'status': 'success', 'collections': collections})
except ValueError as e: except ValueError as e:
return jsonify({'status': 'error', 'message': str(e)}), 400 return jsonify({'status': 'error', 'message': str(e)}), 400
@metadata_bp.route('/ws/test', methods=['POST'])
def test_websocket():
MetaDataManager().socket.emit('test_event', {'data': 'Test message from metadata route'})
return jsonify({'status': 'success'})

View File

@ -1,57 +1,57 @@
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
from audio_io import AudioIO from audio_io import AudioIO
import os import os
recording_bp = Blueprint('recording', __name__) recording_bp = Blueprint('recording', __name__)
@recording_bp.route('/record/start', methods=['POST']) @recording_bp.route('/record/start', methods=['POST'])
def start_recording(): def start_recording():
recorder = AudioIO() recorder = AudioIO()
print('HTTP: Starting audio recording') print('HTTP: Starting audio recording')
recorder.start_recording() recorder.start_recording()
return jsonify({'status': 'recording started'}) return jsonify({'status': 'recording started'})
@recording_bp.route('/record/stop', methods=['POST']) @recording_bp.route('/record/stop', methods=['POST'])
def stop_recording(): def stop_recording():
recorder = AudioIO() recorder = AudioIO()
# print('HTTP: Stopping audio recording') # print('HTTP: Stopping audio recording')
recorder.stop_recording() recorder.stop_recording()
return jsonify({'status': 'recording stopped'}) return jsonify({'status': 'recording stopped'})
@recording_bp.route('/record/save', methods=['POST']) @recording_bp.route('/record/save', methods=['POST'])
def save_recording(): def save_recording():
recorder = AudioIO() recorder = AudioIO()
# print('HTTP: Saving audio recording') # print('HTTP: Saving audio recording')
saved_file = recorder.save_last_n_seconds() saved_file = recorder.save_last_n_seconds()
return jsonify({'status': 'recording saved', 'file': saved_file}) return jsonify({'status': 'recording saved', 'file': saved_file})
@recording_bp.route('/record/status', methods=['GET']) @recording_bp.route('/record/status', methods=['GET'])
def recording_status(): def recording_status():
recorder = AudioIO() recorder = AudioIO()
# print('HTTP: Checking recording status') # print('HTTP: Checking recording status')
status = 'recording' if recorder.is_recording() else 'stopped' status = 'recording' if recorder.is_recording() else 'stopped'
return jsonify({'status': status}) return jsonify({'status': status})
@recording_bp.route('/record/delete', methods=['POST']) @recording_bp.route('/record/delete', methods=['POST'])
def recording_delete(): def recording_delete():
filename = request.json.get('filename') filename = request.json.get('filename')
try: try:
os.remove(filename) os.remove(filename)
return jsonify({'status': 'success'}) return jsonify({'status': 'success'})
except Exception as e: except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 400 return jsonify({'status': 'error', 'message': str(e)}), 400
@recording_bp.route('/playback/start', methods=['POST']) @recording_bp.route('/playback/start', methods=['POST'])
def playback_start(): def playback_start():
print(f"Playing clip") print(f"Playing clip")
# print('HTTP: Starting audio playback') # print('HTTP: Starting audio playback')
clip = request.json clip = request.json
try: try:
io = AudioIO() io = AudioIO()
io.play_clip(clip) io.play_clip(clip)
# os.remove(filename) # os.remove(filename)
return jsonify({'status': 'success'}) return jsonify({'status': 'success'})
except Exception as e: except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 400 return jsonify({'status': 'error', 'message': str(e)}), 400

View File

@ -1,31 +1,32 @@
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
from settings import SettingsManager from settings import SettingsManager
settings_bp = Blueprint('settings', __name__) settings_bp = Blueprint('settings', __name__)
@settings_bp.route('/settings', methods=['GET']) @settings_bp.route('/settings', methods=['GET'])
def get_all_settings(): def get_all_settings():
return jsonify({'status': 'success', 'settings': SettingsManager().get_all_settings()}) return jsonify({'status': 'success', 'settings': SettingsManager().get_all_settings()})
@settings_bp.route('/settings/<name>', methods=['GET']) @settings_bp.route('/settings/<name>', methods=['GET'])
def get_setting(name): def get_setting(name):
value = SettingsManager().get_settings(name) value = SettingsManager().get_settings(name)
if value is not None: if value is not None:
return jsonify({'status': 'success', 'name': name, 'value': value}) return jsonify({'status': 'success', 'name': name, 'value': value})
else: else:
return jsonify({'status': 'error', 'message': f'Setting "{name}" not found'}), 404 return jsonify({'status': 'error', 'message': f'Setting "{name}" not found'}), 404
@settings_bp.route('/settings/update', methods=['POST']) @settings_bp.route('/settings/update', methods=['POST'])
def set_all_settings(): def set_all_settings():
settings = request.json.get('settings') settings = request.json.get('settings')
print (f"Received settings update: {settings}") print (f"Received settings update: {settings}")
if settings is None: if settings is None:
return jsonify({'status': 'error', 'message': 'Settings are required'}), 400 return jsonify({'status': 'error', 'message': 'Settings are required'}), 400
try: try:
for name, value in settings.items(): for name, value in settings.items():
print(f"Updating setting '{name}' to '{value}'") print(f"Updating setting '{name}' to '{value}'")
SettingsManager().set_settings(name, value) SettingsManager().set_settings(name, value)
return jsonify({'status': 'success', 'settings': settings}) return jsonify({'status': 'success', 'settings': settings})
except ValueError as e: except ValueError as e:
return jsonify({'status': 'error', 'message': str(e)}), 400 return jsonify({'status': 'error', 'message': str(e)}), 400

View File

@ -1,10 +1,10 @@
{ {
"input_device": { "input_device": {
"index": 0, "index": 0,
"name": "Microsoft Sound Mapper - Input", "name": "Microsoft Sound Mapper - Input",
"max_input_channels": 2, "max_input_channels": 2,
"default_samplerate": 44100.0 "default_samplerate": 44100.0
}, },
"save_path": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\src\\recordings", "save_path": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\src\\recordings",
"recording_length": 15 "recording_length": 15
} }

View File

@ -1,112 +1,108 @@
import sounddevice as sd import sounddevice as sd
import numpy as np import numpy as np
import comtypes import json
import comtypes.client
from comtypes import CLSCTX_ALL class WindowsAudioManager:
from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume _instance = None
import json
def __new__(cls, *args, **kwargs):
class WindowsAudioManager: if cls._instance is None:
_instance = None cls._instance = super().__new__(cls)
cls._instance.init()
def __new__(cls, *args, **kwargs): return cls._instance
if cls._instance is None: def init(self):
cls._instance = super().__new__(cls) """
cls._instance.init() Initialize Windows audio device and volume management.
return cls._instance """
def init(self): host_apis = sd.query_hostapis()
""" wasapi_device_indexes = None
Initialize Windows audio device and volume management. for api in host_apis:
""" if api['name'].lower() == 'Windows WASAPI'.lower():
host_apis = sd.query_hostapis() wasapi_device_indexes = api['devices']
wasapi_device_indexes = None break
for api in host_apis: # print(f"Host APIs: {host_apis}")
if api['name'].lower() == 'Windows WASAPI'.lower(): # print(f"WASAPI Device Indexes: {wasapi_device_indexes}")
wasapi_device_indexes = api['devices'] wasapi_device_indexes = set(wasapi_device_indexes) if wasapi_device_indexes is not None else set()
break self.devices = [dev for dev in sd.query_devices() if dev['index'] in wasapi_device_indexes]
# print(f"Host APIs: {host_apis}") # self.devices = sd.query_devices()
# print(f"WASAPI Device Indexes: {wasapi_device_indexes}") # print(f"devices: {self.devices}")
wasapi_device_indexes = set(wasapi_device_indexes) if wasapi_device_indexes is not None else set()
self.devices = [dev for dev in sd.query_devices() if dev['index'] in wasapi_device_indexes] self.default_input = sd.default.device[0]
# self.devices = sd.query_devices() self.default_output = sd.default.device[1]
# print(f"devices: {self.devices}")
def list_audio_devices(self, kind='input'):
self.default_input = sd.default.device[0] """
self.default_output = sd.default.device[1] List available audio devices.
def list_audio_devices(self, kind='input'): :param kind: 'input' or 'output'
""" :return: List of audio devices
List available audio devices. """
if kind == 'input':
:param kind: 'input' or 'output' return [
:return: List of audio devices {
""" 'index': dev['index'],
if kind == 'input': 'name': dev['name'],
return [ 'channels': dev['max_input_channels'],
{ 'default_samplerate': dev['default_samplerate']
'index': dev['index'], }
'name': dev['name'], for dev in self.devices if dev['max_input_channels'] > 0
'channels': dev['max_input_channels'], ]
'default_samplerate': dev['default_samplerate'] elif kind == 'output':
} return [
for dev in self.devices if dev['max_input_channels'] > 0 {
] 'index': dev['index'],
elif kind == 'output': 'name': dev['name'],
return [ 'channels': dev['max_output_channels'],
{ 'default_samplerate': dev['default_samplerate']
'index': dev['index'], }
'name': dev['name'], for dev in self.devices if dev['max_output_channels'] > 0
'channels': dev['max_output_channels'], ]
'default_samplerate': dev['default_samplerate'] def get_default_device(self, kind='input'):
} """
for dev in self.devices if dev['max_output_channels'] > 0 Get the default audio device.
]
def get_default_device(self, kind='input'): :param kind: 'input' or 'output'
""" :return: Default audio device information
Get the default audio device. """
if kind == 'input':
:param kind: 'input' or 'output' dev = self.devices[self.default_input]
:return: Default audio device information return [
""" {
if kind == 'input': 'index': dev['index'],
dev = self.devices[self.default_input] 'name': dev['name'],
return [ 'max_input_channels': dev['max_input_channels'],
{ 'default_samplerate': dev['default_samplerate']
'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):
} if(device_index is None):
] return 0
"""
def set_default_input_device(self, device_index): Set the default input audio device.
if(device_index is None):
return 0 :param device_index: Index of the audio device
""" :return: Sample rate of the selected device
Set the default input audio device. """
sd.default.device[0] = device_index
:param device_index: Index of the audio device self.default_input = device_index
:return: Sample rate of the selected device
""" # Get the sample rate of the selected device
sd.default.device[0] = device_index device_info = sd.query_devices(device_index)
self.default_input = device_index return device_info['default_samplerate']
# Get the sample rate of the selected device def set_default_output_device(self, device_index):
device_info = sd.query_devices(device_index) if(device_index is None):
return device_info['default_samplerate'] return self.get_current_output_device_sample_rate()
"""
def set_default_output_device(self, device_index): Set the default output audio device.
if(device_index is None):
return self.get_current_output_device_sample_rate() :param device_index: Index of the audio device
""" :return: Sample rate of the selected device
Set the default output audio device. """
sd.default.device[1] = device_index
:param device_index: Index of the audio device self.default_output = device_index
:return: Sample rate of the selected device
""" # Get the sample rate of the selected device
sd.default.device[1] = device_index device_info = sd.query_devices(device_index)
self.default_output = device_index
# Get the sample rate of the selected device
device_info = sd.query_devices(device_index)
return device_info['default_samplerate'] return device_info['default_samplerate']

View File

@ -18,6 +18,8 @@ import { resolveHtmlPath } from './util';
import registerFileIpcHandlers from '../ipc/audio/main'; import registerFileIpcHandlers from '../ipc/audio/main';
import PythonSubprocessManager from './service'; import PythonSubprocessManager from './service';
const pythonManager = new PythonSubprocessManager('src/main.py');
class AppUpdater { class AppUpdater {
constructor() { constructor() {
log.transports.file.level = 'info'; log.transports.file.level = 'info';
@ -112,9 +114,6 @@ const createWindow = async () => {
registerFileIpcHandlers(); registerFileIpcHandlers();
const pythonManager = new PythonSubprocessManager('src/main.py');
pythonManager.start();
// 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();
@ -127,6 +126,7 @@ const createWindow = async () => {
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
// Respect the OSX convention of having the application in memory even // Respect the OSX convention of having the application in memory even
// after all windows have been closed // after all windows have been closed
pythonManager.stop();
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
app.quit(); app.quit();
} }
@ -135,6 +135,7 @@ app.on('window-all-closed', () => {
app app
.whenReady() .whenReady()
.then(() => { .then(() => {
// pythonManager.start();
createWindow(); createWindow();
app.on('activate', () => { app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the // On macOS it's common to re-create a window in the app when the

View File

@ -40,10 +40,10 @@ export default class PythonSubprocessManager {
}, },
); );
this.process.stdout.on('data', (data: Buffer) => { this.process.stdout.on('data', (data: Buffer) => {
console.log(`Python stdout: ${data.toString()}`); // console.log(`Python stdout: ${data.toString()}`);
}); });
this.process.stderr.on('data', (data: Buffer) => { this.process.stderr.on('data', (data: Buffer) => {
// console.error(`Python stderr: ${data.toString()}`); // console.error(`Python stderr: ${data.toString()}`);
const lines = data.toString().split('\n'); const lines = data.toString().split('\n');
// eslint-disable-next-line no-restricted-syntax // eslint-disable-next-line no-restricted-syntax
for (const line of lines) { for (const line of lines) {

View File

@ -67,32 +67,11 @@ const metadataSlice = createSlice({
targetState.clips.push(clip); targetState.clips.push(clip);
} }
}, },
addNewClips(state, action) { addNewClip(state, action) {
const { collections } = action.payload; const { clip } = action.payload;
Object.keys(collections).forEach((collection) => { state.collections.forEach((collection) => {
const collectionState = state.collections.find( if (collection.name === 'Uncategorized') {
(col) => col.name === collection, collection.clips.push(clip);
);
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);
} }
}); });
}, },
@ -113,6 +92,6 @@ export type RootState = ReturnType<AppStore['getState']>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} // Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = AppStore['dispatch']; export type AppDispatch = AppStore['dispatch'];
export const { setCollections, addNewClips, addCollection } = export const { setCollections, addNewClip, addCollection } =
metadataSlice.actions; metadataSlice.actions;
export default metadataSlice.reducer; export default metadataSlice.reducer;

View File

@ -7,6 +7,7 @@ import { ThemeProvider, createTheme } from '@mui/material/styles';
import DialogTitle from '@mui/material/DialogTitle'; import DialogTitle from '@mui/material/DialogTitle';
import DialogContent from '@mui/material/DialogContent'; import DialogContent from '@mui/material/DialogContent';
import DialogActions from '@mui/material/DialogActions'; import DialogActions from '@mui/material/DialogActions';
import io from 'socket.io-client';
// import 'tailwindcss/tailwind.css'; // import 'tailwindcss/tailwind.css';
import './App.css'; import './App.css';
import ClipList from './components/ClipList'; import ClipList from './components/ClipList';
@ -14,7 +15,7 @@ import { useAppDispatch, useAppSelector } from './hooks';
import { store } from '../redux/main'; import { store } from '../redux/main';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import SettingsPage from './Settings'; import SettingsPage from './Settings';
import apiFetch from './api'; import { apiFetch, getBaseUrl } from './api';
function MainPage() { function MainPage() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -27,20 +28,46 @@ function MainPage() {
const [newCollectionOpen, setNewCollectionOpen] = useState(false); const [newCollectionOpen, setNewCollectionOpen] = useState(false);
const [newCollectionName, setNewCollectionName] = useState<string>(''); const [newCollectionName, setNewCollectionName] = useState<string>('');
const navigate = useNavigate(); const navigate = useNavigate();
const [socket, setSocket] = useState<any>(null);
useEffect(() => {}, []);
useEffect(() => { useEffect(() => {
const fetchMetadata = async () => { const initializeSocket = async () => {
try { const baseUrl = await getBaseUrl();
const response = await apiFetch('meta'); const newSocket = io(baseUrl);
const data = await response.json(); setSocket(newSocket);
dispatch({ type: 'metadata/setAllData', payload: data }); newSocket.on('connect', () => {
} catch (error) { console.log('Connected to WebSocket server');
console.error('Error fetching metadata:', error); });
} newSocket.on('full_data', (data: any) => {
console.log('Received full_data from server:', data);
dispatch({
type: 'metadata/setAllData',
payload: { collections: data },
});
});
newSocket.on('new_clip', (data: any) => {
console.log('Received new_clips from server:', data);
dispatch({
type: 'metadata/addNewClip',
payload: { clip: data },
});
});
}; };
fetchMetadata(); initializeSocket();
const intervalId = setInterval(fetchMetadata, 5000); // const fetchMetadata = async () => {
return () => clearInterval(intervalId); // try {
// const response = await apiFetch('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]); }, [dispatch]);
useEffect(() => { useEffect(() => {

View File

@ -5,7 +5,7 @@ import './App.css';
import TextField from '@mui/material/TextField'; import TextField from '@mui/material/TextField';
import Select from '@mui/material/Select'; import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem'; import MenuItem from '@mui/material/MenuItem';
import apiFetch from './api'; import { apiFetch } from './api';
type AudioDevice = { type AudioDevice = {
index: number; index: number;

View File

@ -1,4 +1,4 @@
const getBaseUrl = async () => { export const getBaseUrl = async () => {
const port = await window.audio.getPort(); const port = await window.audio.getPort();
if (port.error || !port.port) { if (port.error || !port.port) {
return `http://localhost:5010`; return `http://localhost:5010`;
@ -7,7 +7,7 @@ const getBaseUrl = async () => {
return `http://localhost:${port.port}`; return `http://localhost:${port.port}`;
}; };
export default async function apiFetch(endpoint: string, options = {}) { export async function apiFetch(endpoint: string, options = {}) {
const url = `${await getBaseUrl()}/${endpoint}`; const url = `${await getBaseUrl()}/${endpoint}`;
return fetch(url, options); return fetch(url, options);
} }

View File

@ -15,7 +15,7 @@ import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import AudioTrimmer from './AudioTrimer'; import AudioTrimmer from './AudioTrimer';
import { ClipMetadata } from '../../redux/types'; import { ClipMetadata } from '../../redux/types';
import { useAppDispatch, useAppSelector } from '../hooks'; import { useAppDispatch, useAppSelector } from '../hooks';
import apiFetch from '../api'; import { apiFetch } from '../api';
export interface ClipListProps { export interface ClipListProps {
collection: string; collection: string;

View File

@ -1,25 +1,25 @@
 
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 17
VisualStudioVersion = 17.8.34330.188 VisualStudioVersion = 17.8.34330.188
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClipTrimDotNet", "ClipTrimDotNet\ClipTrimDotNet.csproj", "{4635D874-69C0-4010-BE46-77EF92EB1553}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ClipTrimDotNet", "ClipTrimDotNet\ClipTrimDotNet.csproj", "{4635D874-69C0-4010-BE46-77EF92EB1553}"
EndProject EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4635D874-69C0-4010-BE46-77EF92EB1553}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {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}.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.ActiveCfg = Release|Any CPU
{4635D874-69C0-4010-BE46-77EF92EB1553}.Release|Any CPU.Build.0 = Release|Any CPU {4635D874-69C0-4010-BE46-77EF92EB1553}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {926C6896-F36A-4F3B-A9DD-CA3AA48AD99F} SolutionGuid = {926C6896-F36A-4F3B-A9DD-CA3AA48AD99F}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@ -1,14 +1,14 @@
To use: To use:
1. Right click the project and choose "Manage Nuget Packages" 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) 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 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) 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 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...) 6. Before releasing, change the Assembly Information (Right click the project -> Properties -> Application -> Assembly Information...)
For help with StreamDeck-Tools: For help with StreamDeck-Tools:
Discord Server: http://discord.barraider.com Discord Server: http://discord.barraider.com
Resources: Resources:
* StreamDeck-Tools samples and tutorial: https://github.com/BarRaider/streamdeck-tools * StreamDeck-Tools samples and tutorial: https://github.com/BarRaider/streamdeck-tools
* EasyPI library (for working with Property Inspector): https://github.com/BarRaider/streamdeck-easypi * EasyPI library (for working with Property Inspector): https://github.com/BarRaider/streamdeck-easypi

View File

@ -1,22 +1,42 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<configuration> <configuration>
<startup> <startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8"/> <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
</startup> </startup>
<runtime> <runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly> <dependentAssembly>
<assemblyIdentity name="CommandLine" publicKeyToken="5a870481e358d379" culture="neutral"/> <assemblyIdentity name="CommandLine" publicKeyToken="5a870481e358d379" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-2.6.0.0" newVersion="2.6.0.0"/> <bindingRedirect oldVersion="0.0.0.0-2.6.0.0" newVersion="2.6.0.0" />
</dependentAssembly> </dependentAssembly>
<dependentAssembly> <dependentAssembly>
<assemblyIdentity name="System.Drawing.Common" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral"/> <assemblyIdentity name="System.Drawing.Common" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0"/> <bindingRedirect oldVersion="0.0.0.0-8.0.0.0" newVersion="8.0.0.0" />
</dependentAssembly> </dependentAssembly>
<dependentAssembly> <dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral"/> <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0"/> <bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0" />
</dependentAssembly> </dependentAssembly>
</assemblyBinding> <dependentAssembly>
</runtime> <assemblyIdentity name="Microsoft.Extensions.DependencyInjection.Abstractions" publicKeyToken="adb9793829ddae60" culture="neutral" />
</configuration> <bindingRedirect oldVersion="0.0.0.0-10.0.0.2" newVersion="10.0.0.2" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Text.Json" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-10.0.0.2" newVersion="10.0.0.2" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Microsoft.Extensions.Logging.Abstractions" publicKeyToken="adb9793829ddae60" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-10.0.0.2" newVersion="10.0.0.2" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Microsoft.Extensions.Logging" publicKeyToken="adb9793829ddae60" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-10.0.0.2" newVersion="10.0.0.2" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Microsoft.Extensions.DependencyInjection" publicKeyToken="adb9793829ddae60" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-10.0.0.2" newVersion="10.0.0.2" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@ -1 +1 @@
 

View File

@ -1,42 +1,53 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization;
using System; using Newtonsoft.Json.Converters;
using System.Collections.Generic;
using System.Linq; using System;
using System.Text; using System.Collections.Generic;
using System.Threading.Tasks; using System.Linq;
using System.Text;
namespace ClipTrimDotNet.Client using System.Threading.Tasks;
{ using System.Text.Json.Serialization;
public enum PlaybackType
{ namespace ClipTrimDotNet.Client
playStop, {
playOverlap public enum PlaybackType
} {
public class ClipMetadata playStop,
{ playOverlap
[JsonProperty(PropertyName = "filename")] }
public string Filename { get; set; } public class ClipMetadata
{
[JsonProperty(PropertyName = "filename")]
[JsonProperty(PropertyName = "name")] [JsonPropertyName("filename")]
public string Name { get; set; }
public string Filename { get; set; }
[JsonProperty(PropertyName = "volume")]
public double Volume { get; set; } = 1.0; [JsonProperty(PropertyName = "name")]
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonProperty(PropertyName = "startTime")]
public double StartTime { get; set; } = 0.0;
[JsonProperty(PropertyName = "volume")]
[JsonPropertyName("volume")]
[JsonProperty(PropertyName = "endTime")] public double Volume { get; set; } = 1.0;
public double EndTime { get; set; } = 0.0;
[JsonProperty(PropertyName = "startTime")]
[JsonProperty(PropertyName = "playbackType")] [JsonPropertyName("startTime")]
[JsonConverter(typeof(StringEnumConverter))] public double StartTime { get; set; } = 0.0;
public PlaybackType PlaybackType { get; set; } = PlaybackType.playStop;
}
} [JsonProperty(PropertyName = "endTime")]
[JsonPropertyName("endTime")]
public double EndTime { get; set; } = 0.0;
[JsonProperty(PropertyName = "playbackType")]
[Newtonsoft.Json.JsonConverter(typeof(StringEnumConverter))]
[System.Text.Json.Serialization.JsonConverter(typeof(JsonStringEnumConverter))]
[JsonPropertyName("playbackType")]
public PlaybackType PlaybackType { get; set; } = PlaybackType.playStop;
}
}

View File

@ -1,110 +1,287 @@
using System; using BarRaider.SdTools;
using System.Collections.Generic; using ClipTrimDotNet.Keys;
using System.Linq; using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Net.Http; using Newtonsoft.Json;
using System.Text; using SocketIOClient;
using System.Threading.Tasks; using System;
using Newtonsoft.Json; using System.Collections.Generic;
using System.Linq;
namespace ClipTrimDotNet.Client using System.Net.Http;
{ using System.Runtime.CompilerServices;
public class ClipTrimClient using System.Text;
{ using System.Threading.Tasks;
private static ClipTrimClient? instance; using static System.Runtime.InteropServices.JavaScript.JSType;
public static ClipTrimClient Instance
{ namespace ClipTrimDotNet.Client
get {
{ public class ClipTrimClient
if (instance == null) {
{ private static ClipTrimClient? instance;
instance = new ClipTrimClient(); public static ClipTrimClient Instance
} {
return instance; get
} {
} if (instance == null)
{
private HttpClient httpClient; instance = new ClipTrimClient();
}
public ClipTrimClient() return instance;
{ }
httpClient = new HttpClient() }
{
BaseAddress = new Uri("http://localhost:5010/"), //private HttpClient httpClient;
Timeout = TimeSpan.FromSeconds(10) private SocketIO? socket;
};
Task.Run(ShortPoll); public string HostName
} {
get
public async Task ShortPoll() {
{ //return $"http://localhost:5010/";
while (true) return $"http://localhost:{GlobalSettings.Instance.PortNumber}/";
{ }
await GetMetadata(); }
await Task.Delay(TimeSpan.FromSeconds(5)); await Task.Delay(TimeSpan.FromSeconds(5));
private string? currentHostname = null;
}
} void CreateSocket()
{
public List<CollectionMetaData> Collections { get; private set; } = new List<CollectionMetaData>(); Logger.Instance.LogMessage(TracingLevel.INFO, $"Starting ClipTrimClient on port {HostName}");
public CollectionMetaData? SelectedCollection { get; private set; } socket = new SocketIO(new Uri(HostName));
public int PageIndex { get; private set; } = 0; currentHostname = HostName;
private async Task GetMetadata() socket.Options.AutoUpgrade = false;
{ //socket.Options.Path = "/socket.io";
try socket.Options.ConnectionTimeout = TimeSpan.FromSeconds(10);
{ socket.Options.Reconnection = true;
var response = await httpClient.GetAsync("meta"); socket.On("full_data", ctx =>
if (response.IsSuccessStatusCode) {
{ try
var json = await response.Content.ReadAsStringAsync(); {
dynamic collections = JsonConvert.DeserializeObject(json); var response = ctx.GetValue<List<CollectionMetaData>>(0);
collections = collections.collections; Logger.Instance.LogMessage(TracingLevel.INFO, $"full_data event {JsonConvert.SerializeObject(response)}");
Collections = JsonConvert.DeserializeObject<List<CollectionMetaData>>(collections.ToString()); Collections = response!;
} Player.TickAll();
} PageNavigator.TickAll();
catch (Exception ex) //Logger.Instance.LogMessage(TracingLevel.INFO, $"Collections {JsonConvert.SerializeObject(Collections)}");
{ }
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Error pinging ClipTrim API: {ex.Message}"); catch (Exception ex)
return; {
} Logger.Instance.LogMessage(TracingLevel.INFO, $"full_data error {ex.ToString()}");
}
} return Task.CompletedTask;
});
public List<string> GetCollectionNames() socket.On("collection_updated", ctx =>
{ {
//await GetMetadata(); try
return Collections.Select(x => x.Name).ToList(); {
} var response = ctx.GetValue<CollectionMetaData>(0)!;
Logger.Instance.LogMessage(TracingLevel.INFO, $"collection_updated event {JsonConvert.SerializeObject(response)}");
public void SetSelectedCollectionByName(string name) int index = Collections.FindIndex(x => x.Id == response.Id);
{ if (index != -1)
var collection = Collections.FirstOrDefault(x => x.Name == name); {
if (collection != null) Collections[index] = response;
{ Player.TickAll();
SelectedCollection = collection; PageNavigator.TickAll();
PageIndex = 0; }
} }
} catch
{
public ClipMetadata? GetClipByPagedIndex(int index)
{ }
if (SelectedCollection == null) return null;
int clipIndex = PageIndex * 10 + index; return Task.CompletedTask;
if (clipIndex >= 0 && clipIndex < SelectedCollection.Clips.Count) });
{
return SelectedCollection.Clips[clipIndex]; socket.OnConnected += (sender, e) =>
} {
return null; Logger.Instance.LogMessage(TracingLevel.INFO, $"Socket connected: {e}");
} };
public async void PlayClip(ClipMetadata? metadata) socket.OnDisconnected += (sender, e) =>
{ {
if (metadata == null) return; Logger.Instance.LogMessage(TracingLevel.INFO, $"Socket disconnected: {e}");
Task.Run(async () => await Connect());
var response = await httpClient.PostAsync("playback/start", new StringContent(JsonConvert.SerializeObject(metadata), Encoding.UTF8, "application/json")); };
if (!response.IsSuccessStatusCode) Task.Run(async () => await Connect());
{ }
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Error playing clip: {response.ReasonPhrase}");
} public ClipTrimClient()
} {
} //httpClient = new HttpClient()
} //{
// BaseAddress = new Uri("http://localhost:5010/"),
// Timeout = TimeSpan.FromSeconds(10)
//};
CreateSocket();
}
public async Task Connect()
{
if (socket is null) return;
while (!socket.Connected)
{
try
{
await socket.ConnectAsync();
}
catch
{
}
}
}
public List<CollectionMetaData> Collections { get; private set; } = new List<CollectionMetaData>();
public int SelectedCollection { get; private set; } = -1;
public Dictionary<int, int> CollectionIndexes { get; private set; } = new();
public int PageIndex
{
get
{
if (SelectedCollection == -1) return 0;
if (!CollectionIndexes.ContainsKey(SelectedCollection))
{
CollectionIndexes[SelectedCollection] = 0;
}
return CollectionIndexes[SelectedCollection];
}
set
{
if (SelectedCollection == -1) return;
CollectionIndexes[SelectedCollection] = value;
}
}
public bool PageMode { get; set; } = false;
public int PageCount
{
get
{
if (SelectedCollection == -1) return 0;
var collection = Collections[SelectedCollection];
return (collection.Clips.Count - 1) / 10 + 1;
}
}
public bool CanPageUp
{
get
{
if(PageMode) return false;
if (SelectedCollection == -1) return false;
return PageCount - PageIndex > 1;
}
}
public bool CanPageDown
{
get
{
if (PageMode) return false;
return PageIndex > 0;
}
}
public void PageDown()
{
if (CanPageDown)
{
PageIndex--;
}
}
public void PageUp()
{
if (CanPageUp)
{
PageIndex++;
}
}
public List<string> GetCollectionNames()
{
//await GetMetadata();
return Collections.Where(x => x.Name != "Uncategorized").Select(x => x.Name).ToList();
}
public string GetCurrentCollectionName()
{
if (SelectedCollection == -1) return "";
return Collections[SelectedCollection].Name;
}
public string GetPlayerStringByCoordinateIndex(int index)
{
if (PageMode)
{
int pageNumber = index + 1;
if(pageNumber <= PageCount)
{
return pageNumber.ToString();
}
return "";
}
else
{
var collection = GetClipByPagedIndex(index);
return collection?.Name ?? "";
}
}
public ClipMetadata? GetClipByPagedIndex(int index)
{
SelectedCollection = Collections.FindIndex(x => x.Name == GlobalSettings.Instance.ProfileName);
if (SelectedCollection == -1) return null;
int clipIndex = PageIndex * 10 + index;
var collection = Collections[SelectedCollection];
if (clipIndex >= 0 && clipIndex < collection.Clips.Count)
{
return collection.Clips[clipIndex];
}
return null;
}
public async void PlayClip(int index)
{
if (PageMode)
{
if(index < 0 || index >= PageCount) return;
PageIndex = index;
PageMode = false;
Player.TickAll();
PageNavigator.TickAll();
}
else
{
if (socket is null) return;
var metadata = GetClipByPagedIndex(index);
if (metadata == null) return;
//Logger.Instance.LogMessage(TracingLevel.INFO, $"playing clip:");
await socket.EmitAsync("play_clip", new List<object>() { metadata });
}
}
public async void SaveClip()
{
if (socket is null) return;
await socket.EmitAsync("record_clip", new List<object>() { });
}
public async void CheckPort()
{
if (socket is null) return;
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Checking port {socket}");
if (currentHostname != HostName)
{
//Logger.Instance.LogMessage(TracingLevel.INFO, $"port {socket}");
if (socket.Connected)
{
await socket.DisconnectAsync();
}
socket.Dispose();
CreateSocket();
}
}
}
}

View File

@ -1,23 +1,27 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace ClipTrimDotNet.Client
{ namespace ClipTrimDotNet.Client
public class CollectionMetaData {
{ public class CollectionMetaData
[JsonProperty(PropertyName = "name")] {
public string Name { get; set; } [JsonProperty(PropertyName = "name")]
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonProperty(PropertyName = "clips")]
public List<ClipMetadata> Clips { get; set; } = new List<ClipMetadata>();
[JsonProperty(PropertyName = "clips")]
[JsonPropertyName("clips")]
[JsonProperty(PropertyName = "id")] public List<ClipMetadata> Clips { get; set; } = new List<ClipMetadata>();
public int Id { get; set; }
}
} [JsonProperty(PropertyName = "id")]
[JsonPropertyName("id")]
public int Id { get; set; }
}
}

View File

@ -1,162 +1,118 @@
<?xml version="1.0" encoding="utf-8"?> <Project Sdk="Microsoft.NET.Sdk">
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" /> <TargetFramework>net8.0-windows</TargetFramework>
<PropertyGroup> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <OutputType>Exe</OutputType>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> <LangVersion>10</LangVersion>
<ProjectGuid>{4635D874-69C0-4010-BE46-77EF92EB1553}</ProjectGuid> <Nullable>enable</Nullable>
<OutputType>Exe</OutputType> <ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>ClipTrimDotNet</RootNamespace> <PreBuildEvent>npm run stop</PreBuildEvent>
<AssemblyName>ClipTrimDotNet</AssemblyName> <PostBuildEvent>npm run start</PostBuildEvent>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion> <AssemblyTitle>ClipTrimDotNet</AssemblyTitle>
<LangVersion>8</LangVersion> <Product>ClipTrimDotNet</Product>
<FileAlignment>512</FileAlignment> <Copyright>Copyright © 2020</Copyright>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects> <AssemblyVersion>1.0.0.0</AssemblyVersion>
<Deterministic>true</Deterministic> <FileVersion>1.0.0.0</FileVersion>
<Nullable>enable</Nullable> </PropertyGroup>
<TargetFrameworkProfile /> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
</PropertyGroup> <OutputPath>bin\Debug\com.michal-courson.cliptrim.sdPlugin\</OutputPath>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> </PropertyGroup>
<PlatformTarget>AnyCPU</PlatformTarget> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugSymbols>true</DebugSymbols> <OutputPath>bin\Release\ClipTrimDotNet.sdPlugin\</OutputPath>
<DebugType>full</DebugType> </PropertyGroup>
<Optimize>false</Optimize> <ItemGroup>
<OutputPath>bin\Debug\com.michal-courson.cliptrim.sdPlugin\</OutputPath> <None Remove="Images\app_icon.png" />
<DefineConstants>DEBUG;TRACE</DefineConstants> <None Remove="Images\back.png" />
<ErrorReport>prompt</ErrorReport> <None Remove="Images\category_icon.png" />
<WarningLevel>4</WarningLevel> <None Remove="Images\collection.png" />
</PropertyGroup> <None Remove="Images\collection_icon.png" />
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <None Remove="Images\page_nav.png" />
<PlatformTarget>AnyCPU</PlatformTarget> <None Remove="Images\page_nav_icon.png" />
<DebugType>pdbonly</DebugType> <None Remove="Images\player.png" />
<Optimize>true</Optimize> <None Remove="Images\player_icon.png" />
<OutputPath>bin\Release\ClipTrimDotNet.sdPlugin\</OutputPath> <None Remove="Images\record.png" />
<DefineConstants>TRACE</DefineConstants> <None Remove="Images\record_icon.png" />
<ErrorReport>prompt</ErrorReport> <None Remove="manifest.json" />
<WarningLevel>4</WarningLevel> </ItemGroup>
</PropertyGroup> <ItemGroup>
<ItemGroup> <PackageReference Include="CommandLineParser" Version="2.9.1" />
<Reference Include="CommandLine, Version=2.9.1.0, Culture=neutral, PublicKeyToken=5a870481e358d379, processorArchitecture=MSIL"> <PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
<HintPath>..\packages\CommandLineParser.2.9.1\lib\net461\CommandLine.dll</HintPath> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
</Reference> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.2" />
<Reference Include="Microsoft.Win32.Registry, Version=4.1.3.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.2" />
<HintPath>..\packages\Microsoft.Win32.Registry.4.7.0\lib\net461\Microsoft.Win32.Registry.dll</HintPath> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.2" />
</Reference> <PackageReference Include="Microsoft.Extensions.Options" Version="10.0.2" />
<Reference Include="NAudio, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL"> <PackageReference Include="Microsoft.Extensions.Primitives" Version="10.0.2" />
<HintPath>..\packages\NAudio.2.2.1\lib\net472\NAudio.dll</HintPath> <PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
</Reference> <PackageReference Include="NLog" Version="6.0.5" />
<Reference Include="NAudio.Asio, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL"> <PackageReference Include="SocketIOClient" Version="4.0.0.2" />
<HintPath>..\packages\NAudio.Asio.2.2.1\lib\netstandard2.0\NAudio.Asio.dll</HintPath> <PackageReference Include="SocketIOClient.Common" Version="4.0.0" />
</Reference> <PackageReference Include="SocketIOClient.Serializer" Version="4.0.0.1" />
<Reference Include="NAudio.Core, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL"> <PackageReference Include="SocketIOClient.Serializer.NewtonsoftJson" Version="4.0.0.1" />
<HintPath>..\packages\NAudio.Core.2.2.1\lib\netstandard2.0\NAudio.Core.dll</HintPath> <PackageReference Include="StreamDeck-Tools" Version="6.3.2" />
</Reference> <PackageReference Include="System.Diagnostics.DiagnosticSource" Version="10.0.2" />
<Reference Include="NAudio.Midi, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL"> <PackageReference Include="System.Drawing.Common" Version="9.0.10" />
<HintPath>..\packages\NAudio.Midi.2.2.1\lib\netstandard2.0\NAudio.Midi.dll</HintPath> <PackageReference Include="System.IO.Pipelines" Version="10.0.2" />
</Reference> <PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.1.2" />
<Reference Include="NAudio.Wasapi, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL"> <PackageReference Include="System.Security.AccessControl" Version="6.0.1" />
<HintPath>..\packages\NAudio.Wasapi.2.2.1\lib\netstandard2.0\NAudio.Wasapi.dll</HintPath> <PackageReference Include="System.Text.Encodings.Web" Version="10.0.2" />
</Reference> <PackageReference Include="System.Text.Json" Version="10.0.2" />
<Reference Include="NAudio.WinForms, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL"> <PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.3" />
<HintPath>..\packages\NAudio.WinForms.2.2.1\lib\net472\NAudio.WinForms.dll</HintPath> <PackageReference Include="CoreWCF.Primitives" Version="1.8.0" />
</Reference> <PackageReference Include="CoreWCF.ConfigurationManager" Version="1.8.0" />
<Reference Include="NAudio.WinMM, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL"> <PackageReference Include="CoreWCF.Http" Version="1.8.0" />
<HintPath>..\packages\NAudio.WinMM.2.2.1\lib\netstandard2.0\NAudio.WinMM.dll</HintPath> <PackageReference Include="CoreWCF.WebHttp" Version="1.8.0" />
</Reference> <PackageReference Include="CoreWCF.NetTcp" Version="1.8.0" />
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> </ItemGroup>
<HintPath>..\packages\Newtonsoft.Json.13.0.4\lib\net45\Newtonsoft.Json.dll</HintPath> <ItemGroup>
</Reference> <None Update="DialLayout.json">
<Reference Include="NLog, Version=6.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<HintPath>..\packages\NLog.6.0.5\lib\net46\NLog.dll</HintPath> </None>
</Reference> </ItemGroup>
<Reference Include="StreamDeckTools, Version=6.3.2.0, Culture=neutral, processorArchitecture=MSIL"> <ItemGroup>
<HintPath>..\packages\StreamDeck-Tools.6.3.2\lib\netstandard2.0\StreamDeckTools.dll</HintPath> <Content Include="!!README!!.txt" />
</Reference> <Content Include="Images\app_icon.png">
<Reference Include="System" /> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Reference Include="System.Configuration" /> </Content>
<Reference Include="System.Core" /> <Content Include="Images\back.png">
<Reference Include="System.Drawing" /> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Reference Include="System.Drawing.Common, Version=9.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL"> </Content>
<HintPath>..\packages\System.Drawing.Common.9.0.10\lib\net462\System.Drawing.Common.dll</HintPath> <Content Include="Images\category_icon.png">
</Reference> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Reference Include="System.IO.Compression" /> </Content>
<Reference Include="System.Runtime.Serialization" /> <Content Include="Images\collection.png">
<Reference Include="System.Security.AccessControl, Version=4.1.3.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<HintPath>..\packages\System.Security.AccessControl.4.7.0\lib\net461\System.Security.AccessControl.dll</HintPath> </Content>
</Reference> <Content Include="Images\collection_icon.png">
<Reference Include="System.Security.Principal.Windows, Version=4.1.3.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<HintPath>..\packages\System.Security.Principal.Windows.4.7.0\lib\net461\System.Security.Principal.Windows.dll</HintPath> </Content>
</Reference> <Content Include="Images\page_nav.png">
<Reference Include="System.ServiceModel" /> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Reference Include="System.Transactions" /> </Content>
<Reference Include="System.Xml.Linq" /> <Content Include="Images\page_nav_icon.png">
<Reference Include="System.Data.DataSetExtensions" /> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Reference Include="Microsoft.CSharp" /> </Content>
<Reference Include="System.Data" /> <Content Include="Images\player_icon.png">
<Reference Include="System.Net.Http" /> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Reference Include="System.Xml" /> </Content>
</ItemGroup> <Content Include="Images\record.png">
<ItemGroup> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Compile Include="BaseTest.cs" /> </Content>
<Compile Include="Client\ClipMetadata.cs" /> <Content Include="Images\player.png">
<Compile Include="Client\ClipTrimClient.cs" /> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Compile Include="Client\CollectionMetaData.cs" /> </Content>
<Compile Include="GlobalSettings.cs" /> <Content Include="Images\record_icon.png">
<Compile Include="Player.cs" /> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Compile Include="ProfileSwitcher.cs" /> </Content>
<Compile Include="Program.cs" /> <Content Include="manifest.json">
<Compile Include="Properties\AssemblyInfo.cs" /> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Compile Include="WavPlayer.cs" /> </Content>
</ItemGroup> <Content Include="package.json" />
<ItemGroup> <Content Include="PropertyInspector\profile_swticher.html">
<None Include="App.config" /> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<None Include="DialLayout.json"> </Content>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <Content Include="PropertyInspector\file_player.html">
</None> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<None Include="manifest.json"> </Content>
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </ItemGroup>
</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> </Project>

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Debug|AnyCPU'">
<StartArguments>--port 23654 --pluginUUID com.michal-courson.cliptrim --registerEvent restart --info {}</StartArguments> <StartArguments>--port 23654 --pluginUUID com.michal-courson.cliptrim --registerEvent restart --info {}</StartArguments>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@ -1,40 +1,40 @@
{ {
"id": "sampleDial", "id": "sampleDial",
"items": [ "items": [
{ {
"key": "title", "key": "title",
"type": "text", "type": "text",
"rect": [ 16, 10, 136, 24 ], "rect": [ 16, 10, 136, 24 ],
"font": { "font": {
"size": 16, "size": 16,
"weight": 600 "weight": 600
}, },
"alignment": "left" "alignment": "left"
}, },
{ {
"key": "icon", "key": "icon",
"type": "pixmap", "type": "pixmap",
"rect": [ 16, 40, 48, 48 ] "rect": [ 16, 40, 48, 48 ]
}, },
{ {
"key": "value", "key": "value",
"type": "text", "type": "text",
"rect": [ 76, 40, 108, 32 ], "rect": [ 76, 40, 108, 32 ],
"font": { "font": {
"size": 24, "size": 24,
"weight": 600 "weight": 600
}, },
"alignment": "right" "alignment": "right"
}, },
{ {
"key": "indicator", "key": "indicator",
"type": "gbar", "type": "gbar",
"rect": [ 76, 74, 108, 20 ], "rect": [ 76, 74, 108, 20 ],
"value": 0, "value": 0,
"subtype": 4, "subtype": 4,
"bar_h": 12, "bar_h": 12,
"border_w": 0, "border_w": 0,
"bar_bg_c": "0:#427018,0.75:#705B1C,0.90:#702735,1:#702735" "bar_bg_c": "0:#427018,0.75:#705B1C,0.90:#702735,1:#702735"
} }
] ]
} }

View File

@ -1,109 +1,49 @@
using BarRaider.SdTools; using BarRaider.SdTools;
using Newtonsoft.Json; using Newtonsoft.Json;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using BarRaider.SdTools.Wrappers; using BarRaider.SdTools.Wrappers;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NAudio.MediaFoundation;
namespace ClipTrimDotNet
namespace ClipTrimDotNet {
{ public class GlobalSettings
public class FileEntry {
{ public static GlobalSettings? _inst;
public FileEntry() public static GlobalSettings Instance
{ {
Volume = 1.0; get
Playtype = "Play/Overlap"; {
} _inst ??= CreateDefaultSettings();
[JsonProperty(PropertyName = "Volume")] return _inst;
public double Volume { get; set; } }
[JsonProperty(PropertyName = "Playtype")] set
public string Playtype { get; set; } {
} _inst = value;
public class CollectionEntry }
{ }
public CollectionEntry() public static GlobalSettings CreateDefaultSettings()
{ {
Files = new Dictionary<string, FileEntry>(); GlobalSettings instance = new GlobalSettings();
} instance.ProfileName = null;
[JsonProperty(PropertyName = "Files")] instance.PortNumber = 5010;
public Dictionary<string, FileEntry> Files { get; set; } return instance;
} }
public class GlobalSettings
{ [JsonProperty(PropertyName = "profileName")]
public static GlobalSettings? _inst; public string? ProfileName { get; set; }
public static GlobalSettings Instance
{ [JsonProperty(PropertyName = "portNumber")]
get public int? PortNumber { get; set; }
{
_inst ??= CreateDefaultSettings();
return _inst; public void SetCurrentProfile(string profile)
} {
set ProfileName = profile;
{ }
_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;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 B

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,49 @@
using BarRaider.SdTools;
using ClipTrimDotNet.Client;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ClipTrimDotNet.Keys
{
[PluginActionId("com.michal-courson.cliptrim.clip-save")]
public class ClipSave : KeypadBase
{
public ClipSave(SDConnection connection, InitialPayload payload) : base(connection, payload)
{
GlobalSettingsManager.Instance.RequestGlobalSettings();
}
public void Instance_OnReceivedGlobalSettings(object sender, ReceivedGlobalSettingsPayload e)
{
Tools.AutoPopulateSettings(GlobalSettings.Instance, e.Settings);
}
public override void Dispose()
{
}
public override void KeyPressed(KeyPayload payload)
{
ClipTrimClient.Instance.SaveClip();
}
public override void OnTick()
{
}
public override void KeyReleased(KeyPayload payload)
{
}
public override void ReceivedSettings(ReceivedSettingsPayload payload)
{
}
public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload)
{
Tools.AutoPopulateSettings(GlobalSettings.Instance, payload.Settings);
}
}
}

View File

@ -0,0 +1,107 @@
using BarRaider.SdTools;
using ClipTrimDotNet.Client;
using Microsoft.AspNetCore.Authentication;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ClipTrimDotNet.Keys
{
[PluginActionId("com.michal-courson.cliptrim.page-navigator")]
public class PageNavigator : KeypadBase
{
static List<PageNavigator> instances = new();
private KeyCoordinates coordinates;
public PageNavigator(SDConnection connection, InitialPayload payload) : base(connection, payload)
{
coordinates = payload.Coordinates;
GlobalSettingsManager.Instance.RequestGlobalSettings();
instances.Add(this);
OnTick();
}
public void Instance_OnReceivedGlobalSettings(object sender, ReceivedGlobalSettingsPayload e)
{
Tools.AutoPopulateSettings(GlobalSettings.Instance, e.Settings);
}
public int GetIndex()
{
int index = Math.Min(Math.Max(coordinates.Column - 1, 0), 2);
return index;
}
public override void Dispose()
{
}
public override void KeyPressed(KeyPayload payload)
{
switch (GetIndex())
{
case 0:
ClipTrimClient.Instance.PageDown();
break;
case 1:
ClipTrimClient.Instance.PageMode = !ClipTrimClient.Instance.PageMode;
break;
case 2:
ClipTrimClient.Instance.PageUp();
break;
}
Player.TickAll();
TickAll();
}
public static void TickAll()
{
foreach (var instance in instances)
{
instance.OnTick();
}
instances.RemoveAll(i => i == null);
}
private async void SetTitle()
{
switch (GetIndex())
{
case 0:
await Connection.SetTitleAsync(ClipTrimClient.Instance.CanPageDown ? "<" : "");
break;
case 1:
await Connection.SetTitleAsync(ClipTrimClient.Instance.GetCurrentCollectionName() + "\n" + (ClipTrimClient.Instance.PageIndex + 1).ToString());
break;
case 2:
await Connection.SetTitleAsync(ClipTrimClient.Instance.CanPageUp ? ">" : "");
break;
}
}
public override void OnTick()
{
SetTitle();
}
public override void KeyReleased(KeyPayload payload)
{
}
public override void ReceivedSettings(ReceivedSettingsPayload payload)
{
}
public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload)
{
Tools.AutoPopulateSettings(GlobalSettings.Instance, payload.Settings);
//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>();
//}
}
}
}

View File

@ -0,0 +1,87 @@
using BarRaider.SdTools;
using BarRaider.SdTools.Wrappers;
using ClipTrimDotNet.Client;
using System.Text.Json.Serialization;
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.Keys
{
[PluginActionId("com.michal-courson.cliptrim.player")]
public class Player : KeypadBase
{
private int coordIndex;
private string? current_text = null;
private KeyCoordinates coordinates;
static List<Player> instances = new();
public Player(SDConnection connection, InitialPayload payload) : base(connection, payload)
{
coordinates = payload.Coordinates;
GlobalSettingsManager.Instance.RequestGlobalSettings();
instances.Add(this);
coordIndex = Math.Max((coordinates.Row - 1) * 5 + coordinates.Column, 0);
OnTick();
}
public static void TickAll()
{
Logger.Instance.LogMessage(TracingLevel.INFO, "Ticking all Player instances" + instances.Count);
foreach (var instance in instances)
{
instance.OnTick();
}
instances.RemoveAll(i => i == null);
}
private async void CheckFile()
{
var next_text = ClipTrimClient.Instance.GetPlayerStringByCoordinateIndex(coordIndex);
if(next_text != current_text)
{
current_text = next_text;
await Connection.SetTitleAsync($"{current_text ?? ""}");
}
current_text = next_text;
}
public override void Dispose()
{
}
public override void KeyPressed(KeyPayload payload)
{
ClipTrimClient.Instance.PlayClip(coordIndex);
}
public override void OnTick() {
CheckFile();
}
public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload) {
Tools.AutoPopulateSettings(GlobalSettings.Instance, payload.Settings);
}
public override void KeyReleased(KeyPayload payload)
{
}
public override void ReceivedSettings(ReceivedSettingsPayload payload)
{
}
}
}

View File

@ -0,0 +1,140 @@
using BarRaider.SdTools;
using BarRaider.SdTools.Wrappers;
using ClipTrimDotNet.Client;
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.Keys
{
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)
{
settings = PluginSettings.CreateDefaultSettings();
SaveSettings();
}
else
{
settings = payload.Settings.ToObject<PluginSettings>()!;
}
Logger.Instance.LogMessage(TracingLevel.INFO, $"ProfileSwitcher initialized with payload {JsonConvert.SerializeObject(payload)}");
GlobalSettingsManager.Instance.RequestGlobalSettings();
Connection.OnSendToPlugin += Connection_OnSendToPlugin;
SetTitle();
}
private async void SetTitle()
{
await Connection.SetTitleAsync(settings.ProfileName);
}
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"] is null) return;
if (e.Event.Payload["event"]!.ToString() == "getProfiles")
{
var files = ClipTrimClient.Instance.GetCollectionNames();
var items = files.Select(x => new DataSourceItem { label = x, value = x});
var obj = new JObject
{
["event"] = "getProfiles",
["items"] = JArray.FromObject(items)
};
await Connection.SendToPropertyInspectorAsync(obj);
}
}
public override void Dispose()
{
}
public override async void KeyPressed(KeyPayload payload)
{
GlobalSettings.Instance.SetCurrentProfile(settings.ProfileName??"");
ClipTrimClient.Instance.PageMode = false;
PageNavigator.TickAll();
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)
{
//Logger.Instance.LogMessage(TracingLevel.INFO, $"ProfileSwitcher received settings {JsonConvert.SerializeObject(payload.Settings)}");
Tools.AutoPopulateSettings(settings, payload.Settings);
SaveSettings();
}
public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload)
{
Tools.AutoPopulateSettings(GlobalSettings.Instance, payload.Settings);
ClipTrimClient.Instance.CheckPort();
//if (payload.Settings == null || payload.Settings.Count == 0)
//{
// var inst = GlobalSettings.Instance;
//}
//else
//{
// GlobalSettings.Instance = payload.Settings.ToObject<GlobalSettings>();
//}
}
#region Private Methods
private Task SaveSettings()
{
return Connection.SetSettingsAsync(JObject.FromObject(settings));
}
#endregion
}
}

View File

@ -1,193 +0,0 @@
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
}
}

View File

@ -1,167 +0,0 @@
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
}
}

View File

@ -1,19 +1,20 @@
using BarRaider.SdTools; using BarRaider.SdTools;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace ClipTrimDotNet namespace ClipTrimDotNet
{ {
internal class Program internal class Program
{ {
static void Main(string[] args) static void Main(string[] args)
{ {
// Uncomment this line of code to allow for debugging // Uncomment this line of code to allow for debugging
//while (!System.Diagnostics.Debugger.IsAttached) { System.Threading.Thread.Sleep(100); } //while (!System.Diagnostics.Debugger.IsAttached) { System.Threading.Thread.Sleep(100); }
SDWrapper.Run(args); //Client.ClipTrimClient.Instance.PortNumber = 5010;
} SDWrapper.Run(args);
} }
} }
}

View File

@ -1,36 +1,13 @@
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
[assembly: AssemblyTrademark("")]
// General Information about an assembly is controlled through the following [assembly: AssemblyCulture("")]
// set of attributes. Change these attribute values to modify the information
// associated with an assembly. // Setting ComVisible to false makes the types in this assembly not visible
[assembly: AssemblyTitle("ClipTrimDotNet")] // to COM components. If you need to access a type in this assembly from
[assembly: AssemblyDescription("")] // COM, set the ComVisible attribute to true on that type.
[assembly: AssemblyConfiguration("")] [assembly: ComVisible(false)]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("ClipTrimDotNet")] // The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: AssemblyCopyright("Copyright © 2020")] [assembly: Guid("1bb90885-9d98-46ef-b983-4a4ef3aea890")]
[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")]

View File

@ -1,45 +1,45 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head lang="en"> <head lang="en">
<title>Increment Counter Settings</title> <title>Increment Counter Settings</title>
<meta charset="utf-8" /> <meta charset="utf-8" />
<script src="https://sdpi-components.dev/releases/v3/sdpi-components.js"></script> <script src="https://sdpi-components.dev/releases/v3/sdpi-components.js"></script>
</head> </head>
<body> <body>
<!-- <!--
Learn more about property inspector components at https://sdpi-components.dev/docs/components Learn more about property inspector components at https://sdpi-components.dev/docs/components
--> -->
<sdpi-item label="Index"> <!--<sdpi-item label="Index">
<sdpi-select setting="index" placeholder="file index"> <sdpi-select setting="index" placeholder="file index">
<option value="0">0</option> <option value="0">0</option>
<option value="1">1</option> <option value="1">1</option>
<option value="2">2</option> <option value="2">2</option>
<option value="3">3</option> <option value="3">3</option>
<option value="4">4</option> <option value="4">4</option>
<option value="5">5</option> <option value="5">5</option>
<option value="6">6</option> <option value="6">6</option>
<option value="7">7</option> <option value="7">7</option>
<option value="8">8</option> <option value="8">8</option>
<option value="9">9</option> <option value="9">9</option>
<option value="10">10</option> <option value="10">10</option>
<option value="11">11</option> <option value="11">11</option>
<option value="12">12</option> <option value="12">12</option>
<option value="13">13</option> <option value="13">13</option>
<option value="14">14</option> <option value="14">14</option>
<option value="15">15</option> <option value="15">15</option>
</sdpi-select> </sdpi-select>
</sdpi-item> </sdpi-item>
<sdpi-item label="Play Mode"> <sdpi-item label="Play Mode">
<sdpi-select setting="playtype" placeholder="Play Mode"> <sdpi-select setting="playtype" placeholder="Play Mode">
<option value="Play/Overlap">Play/Overlap</option> <option value="Play/Overlap">Play/Overlap</option>
<option value="Play/Stop">Play/Stop</option> <option value="Play/Stop">Play/Stop</option>
</sdpi-select> </sdpi-select>
</sdpi-item> </sdpi-item>
<sdpi-item label="Volume"> <sdpi-item label="Volume">
<sdpi-range setting="volume" min=".1" max="1" , step="0.05"></sdpi-range> <sdpi-range setting="volume" min=".1" max="1" , step="0.05"></sdpi-range>
</sdpi-item> </sdpi-item>-->
</body> </body>
</html> </html>

View File

@ -1,34 +1,41 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head lang="en"> <head lang="en">
<title>Increment Counter Settings</title> <title>Increment Counter Settings</title>
<meta charset="utf-8" /> <meta charset="utf-8" />
<script src="https://sdpi-components.dev/releases/v3/sdpi-components.js"></script> <script src="https://sdpi-components.dev/releases/v3/sdpi-components.js"></script>
</head> </head>
<body> <body>
<!-- <!--
Learn more about property inspector components at https://sdpi-components.dev/docs/components Learn more about property inspector components at https://sdpi-components.dev/docs/components
--> -->
<sdpi-item label="Profile Name"> <sdpi-item label="Profile Name">
<sdpi-select setting="profileName" <sdpi-select setting="profileName"
datasource="getProfiles" datasource="getProfiles"
show-refresh="true" show-refresh="true"
placeholder="Select a ClipTrim page"></sdpi-select> placeholder="Select a ClipTrim page"></sdpi-select>
</sdpi-item> </sdpi-item>
<sdpi-item label="Base Path"> <sdpi-item>
<sdpi-file setting="basePath" <sdpi-textfield
global="true" global="true"
webkitdirectory setting="portNumber"
directory placeholder="Port Number"
multiple></sdpi-file> pattern="[0-9]+"></sdpi-textfield>
</sdpi-item> </sdpi-item>
<sdpi-item label="Output Device"> <!--<sdpi-item label="Base Path">
<sdpi-select setting="outputDevice" <sdpi-file setting="basePath"
global="true" global="true"
datasource="getOutputDevices" webkitdirectory
show-refresh="true" directory
placeholder="Select an Ouput Device"></sdpi-select> multiple></sdpi-file>
</sdpi-item> </sdpi-item>
</body> <sdpi-item label="Output Device">
</html> <sdpi-select setting="outputDevice"
global="true"
datasource="getOutputDevices"
show-refresh="true"
placeholder="Select an Ouput Device"></sdpi-select>
</sdpi-item>-->
</body>
</html>

View File

@ -1,225 +0,0 @@
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();
}
}

View File

@ -1,62 +1,92 @@
{ {
"Actions": [ "Actions": [
{ {
"Icon": "Images/icon", "Icon": "Images/player_icon",
"Name": "Player", "Name": "Player",
"States": [ "States": [
{ {
"Image": "Images/pluginAction", "Image": "Images/player",
"TitleAlignment": "middle", "TitleAlignment": "middle",
"FontSize": 11 "FontSize": 11
} }
], ],
"SupportedInMultiActions": false, "SupportedInMultiActions": false,
"Tooltip": "Plays a bound audio file", "Tooltip": "Plays a bound audio file",
"UUID": "com.michal-courson.cliptrim.player", "UUID": "com.michal-courson.cliptrim.player",
"PropertyInspectorPath": "PropertyInspector/file_player.html" "PropertyInspectorPath": "PropertyInspector/file_player.html"
}, },
{ {
"Icon": "Images/icon", "Icon": "Images/collection_icon",
"Name": "Profile Switcher", "Name": "Collection Selector",
"States": [ "States": [
{ {
"Image": "Images/pluginAction", "Image": "Images/collection",
"TitleAlignment": "middle", "TitleAlignment": "middle",
"FontSize": 11 "FontSize": 11
} }
], ],
"SupportedInMultiActions": false, "SupportedInMultiActions": false,
"Tooltip": "Selects which sub folder to use and opens effect profile", "Tooltip": "Selects which collection to use",
"UUID": "com.michal-courson.cliptrim.profile-switcher", "UUID": "com.michal-courson.cliptrim.profile-switcher",
"PropertyInspectorPath": "PropertyInspector/profile_swticher.html" "PropertyInspectorPath": "PropertyInspector/profile_swticher.html"
} },
], {
"Author": "Michal Courson", "Icon": "Images/page_nav_icon",
"Name": "ClipTrimDotNet", "Name": "Page Navigator",
"Description": "Pairs with cliptrim for easy voice recording and trimming", "States": [
"Icon": "Images/pluginIcon", {
"Version": "0.1.0.0", "Image": "Images/page_nav",
"CodePath": "ClipTrimDotNet.exe", "TitleAlignment": "middle",
"Category": "ClipTrimDotNet", "FontSize": 16
"CategoryIcon": "Images/categoryIcon", }
"UUID": "com.michal-courson.cliptrim", ],
"OS": [ "SupportedInMultiActions": false,
{ "Tooltip": "Navigates pages",
"Platform": "windows", "UUID": "com.michal-courson.cliptrim.page-navigator",
"MinimumVersion": "10" "PropertyInspectorPath": "PropertyInspector/file_player.html"
} },
], {
"SDKVersion": 2, "Icon": "Images/record_icon",
"Software": { "Name": "Save Clip",
"MinimumVersion": "6.4" "States": [
}, {
"Profiles": [ "Image": "Images/record",
{ "TitleAlignment": "middle",
"Name": "ClipTrim", "FontSize": 16
"DeviceType": 0, }
"Readonly": false, ],
"DontAutoSwitchWhenInstalled": false "SupportedInMultiActions": false,
} "Tooltip": "Saves the current clip",
] "UUID": "com.michal-courson.cliptrim.clip-save",
"PropertyInspectorPath": "PropertyInspector/file_player.html"
}
],
"Author": "Michal Courson",
"Name": "ClipTrimDotNet",
"Description": "Pairs with cliptrim for easy voice recording and trimming",
"Icon": "Images/app_icon",
"Version": "0.1.0.0",
"CodePath": "ClipTrimDotNet.exe",
"Category": "ClipTrim",
"CategoryIcon": "Images/category_icon",
"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
}
]
} }

View File

@ -1,272 +1,272 @@
{ {
"name": "ClipTrimDotNet", "name": "ClipTrimDotNet",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"devDependencies": { "devDependencies": {
"shx": "^0.3.4" "shx": "^0.3.4"
} }
}, },
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.11", "version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0", "balanced-match": "^1.0.0",
"concat-map": "0.0.1" "concat-map": "0.0.1"
} }
}, },
"node_modules/concat-map": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fs.realpath": { "node_modules/fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/glob": { "node_modules/glob": {
"version": "7.2.3", "version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported", "deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"fs.realpath": "^1.0.0", "fs.realpath": "^1.0.0",
"inflight": "^1.0.4", "inflight": "^1.0.4",
"inherits": "2", "inherits": "2",
"minimatch": "^3.1.1", "minimatch": "^3.1.1",
"once": "^1.3.0", "once": "^1.3.0",
"path-is-absolute": "^1.0.0" "path-is-absolute": "^1.0.0"
}, },
"engines": { "engines": {
"node": "*" "node": "*"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/inflight": { "node_modules/inflight": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "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.", "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, "dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"once": "^1.3.0", "once": "^1.3.0",
"wrappy": "1" "wrappy": "1"
} }
}, },
"node_modules/inherits": { "node_modules/inherits": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/interpret": { "node_modules/interpret": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
"integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/is-core-module": { "node_modules/is-core-module": {
"version": "2.16.1", "version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"hasown": "^2.0.2" "hasown": "^2.0.2"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/minimatch": { "node_modules/minimatch": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^1.1.7" "brace-expansion": "^1.1.7"
}, },
"engines": { "engines": {
"node": "*" "node": "*"
} }
}, },
"node_modules/minimist": { "node_modules/minimist": {
"version": "1.2.8", "version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/once": { "node_modules/once": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"wrappy": "1" "wrappy": "1"
} }
}, },
"node_modules/path-is-absolute": { "node_modules/path-is-absolute": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/path-parse": { "node_modules/path-parse": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/rechoir": { "node_modules/rechoir": {
"version": "0.6.2", "version": "0.6.2",
"resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
"integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"resolve": "^1.1.6" "resolve": "^1.1.6"
}, },
"engines": { "engines": {
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.10", "version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"is-core-module": "^2.16.0", "is-core-module": "^2.16.0",
"path-parse": "^1.0.7", "path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0" "supports-preserve-symlinks-flag": "^1.0.0"
}, },
"bin": { "bin": {
"resolve": "bin/resolve" "resolve": "bin/resolve"
}, },
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/shelljs": { "node_modules/shelljs": {
"version": "0.8.5", "version": "0.8.5",
"resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz",
"integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==",
"dev": true, "dev": true,
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"glob": "^7.0.0", "glob": "^7.0.0",
"interpret": "^1.0.0", "interpret": "^1.0.0",
"rechoir": "^0.6.2" "rechoir": "^0.6.2"
}, },
"bin": { "bin": {
"shjs": "bin/shjs" "shjs": "bin/shjs"
}, },
"engines": { "engines": {
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/shx": { "node_modules/shx": {
"version": "0.3.4", "version": "0.3.4",
"resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz", "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz",
"integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==", "integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"minimist": "^1.2.3", "minimist": "^1.2.3",
"shelljs": "^0.8.5" "shelljs": "^0.8.5"
}, },
"bin": { "bin": {
"shx": "lib/cli.js" "shx": "lib/cli.js"
}, },
"engines": { "engines": {
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/supports-preserve-symlinks-flag": { "node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 0.4" "node": ">= 0.4"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/wrappy": { "node_modules/wrappy": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
} }
} }
} }

View File

@ -1,14 +1,14 @@
{ {
"scripts": { "scripts": {
"stop": "streamdeck stop com.michal-courson.cliptrim", "stop": "streamdeck stop com.michal-courson.cliptrim",
"copy": "@powershell robocopy bin/Debug/ClipTrimDotNet.sdPlugin bin/Debug/com.michal-courson.cliptrim.sdPlugin", "copy": "@powershell robocopy bin/Debug/ClipTrimDotNet.sdPlugin bin/Debug/com.michal-courson.cliptrim.sdPlugin/net8.0-windows",
"link": "streamdeck link bin/Debug/com.michal-courson.cliptrim.sdPlugin", "link": "streamdeck link bin/Debug/com.michal-courson.cliptrim.sdPlugin",
"restart": "streamdeck restart com.michal-courson.cliptrim", "restart": "streamdeck restart com.michal-courson.cliptrim",
"start": "npm run link && npm run restart", "start": "npm run link && npm run restart",
"all": "npm run stop && npm run copy && 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/" "pack": "streamdeck pack bin/Debug/com.michal-courson.cliptrim.sdPlugin/net8.0-windows/"
}, },
"devDependencies": { "devDependencies": {
"shx": "^0.3.4" "shx": "^0.3.4"
} }
} }

View File

@ -1,18 +1,46 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<packages> <packages>
<package id="CommandLineParser" version="2.9.1" targetFramework="net472" /> <package id="CommandLineParser" version="2.9.1" targetFramework="net472" />
<package id="Microsoft.Win32.Registry" version="4.7.0" targetFramework="net48" /> <package id="Microsoft.Bcl.AsyncInterfaces" version="10.0.2" targetFramework="net48" />
<package id="NAudio" version="2.2.1" targetFramework="net48" /> <package id="Microsoft.Extensions.DependencyInjection" version="10.0.2" targetFramework="net48" />
<package id="NAudio.Asio" version="2.2.1" targetFramework="net48" /> <package id="Microsoft.Extensions.DependencyInjection.Abstractions" version="10.0.2" targetFramework="net48" />
<package id="NAudio.Core" version="2.2.1" targetFramework="net48" /> <package id="Microsoft.Extensions.Logging" version="10.0.2" targetFramework="net48" />
<package id="NAudio.Midi" version="2.2.1" targetFramework="net48" /> <package id="Microsoft.Extensions.Logging.Abstractions" version="10.0.2" targetFramework="net48" />
<package id="NAudio.Wasapi" version="2.2.1" targetFramework="net48" /> <package id="Microsoft.Extensions.Options" version="10.0.2" targetFramework="net48" />
<package id="NAudio.WinForms" version="2.2.1" targetFramework="net48" /> <package id="Microsoft.Extensions.Primitives" version="10.0.2" targetFramework="net48" />
<package id="NAudio.WinMM" version="2.2.1" targetFramework="net48" /> <package id="Microsoft.Win32.Registry" version="4.7.0" targetFramework="net48" />
<package id="Newtonsoft.Json" version="13.0.4" targetFramework="net48" /> <package id="NAudio" version="2.2.1" targetFramework="net48" />
<package id="NLog" version="6.0.5" targetFramework="net48" /> <package id="NAudio.Asio" version="2.2.1" targetFramework="net48" />
<package id="StreamDeck-Tools" version="6.3.2" targetFramework="net48" /> <package id="NAudio.Core" version="2.2.1" targetFramework="net48" />
<package id="System.Drawing.Common" version="9.0.10" targetFramework="net48" /> <package id="NAudio.Midi" version="2.2.1" targetFramework="net48" />
<package id="System.Security.AccessControl" version="4.7.0" targetFramework="net48" /> <package id="NAudio.Wasapi" version="2.2.1" targetFramework="net48" />
<package id="System.Security.Principal.Windows" version="4.7.0" 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="SocketIOClient" version="4.0.0.2" targetFramework="net48" />
<package id="SocketIOClient.Common" version="4.0.0" targetFramework="net48" />
<package id="SocketIOClient.Serializer" version="4.0.0.1" targetFramework="net48" />
<package id="SocketIOClient.Serializer.NewtonsoftJson" version="4.0.0.1" targetFramework="net48" />
<package id="StreamDeck-Tools" version="6.3.2" targetFramework="net48" />
<package id="System.Buffers" version="4.6.1" targetFramework="net48" />
<package id="System.Diagnostics.DiagnosticSource" version="10.0.2" targetFramework="net48" />
<package id="System.Drawing.Common" version="9.0.10" targetFramework="net48" />
<package id="System.IO" version="4.3.0" targetFramework="net48" />
<package id="System.IO.Pipelines" version="10.0.2" targetFramework="net48" />
<package id="System.Memory" version="4.6.3" targetFramework="net48" />
<package id="System.Net.Http" version="4.3.4" targetFramework="net48" />
<package id="System.Numerics.Vectors" version="4.6.1" targetFramework="net48" />
<package id="System.Runtime" version="4.3.0" targetFramework="net48" />
<package id="System.Runtime.CompilerServices.Unsafe" version="6.1.2" targetFramework="net48" />
<package id="System.Security.AccessControl" version="4.7.0" targetFramework="net48" />
<package id="System.Security.Cryptography.Algorithms" version="4.3.0" targetFramework="net48" />
<package id="System.Security.Cryptography.Encoding" version="4.3.0" targetFramework="net48" />
<package id="System.Security.Cryptography.Primitives" version="4.3.0" targetFramework="net48" />
<package id="System.Security.Cryptography.X509Certificates" version="4.3.0" targetFramework="net48" />
<package id="System.Security.Principal.Windows" version="4.7.0" targetFramework="net48" />
<package id="System.Text.Encodings.Web" version="10.0.2" targetFramework="net48" />
<package id="System.Text.Json" version="10.0.2" targetFramework="net48" />
<package id="System.Threading.Tasks.Extensions" version="4.6.3" targetFramework="net48" />
<package id="System.ValueTuple" version="4.6.1" targetFramework="net48" />
</packages> </packages>