20 Commits

Author SHA1 Message Date
ad07bf7fe6 remove client test app 2026-02-26 15:52:04 -05:00
bc40f9abe3 socket set up properly 2026-02-26 15:48:41 -05:00
e7f649ae0b * 'Line 1: Added 'using Newtonsoft.Json.Serialization;' to ensure that JsonPropertyAttribute is available, as it is defined in this namespace in Newtonsoft.Json.' in file 'stream_deck_plugin\ClipTrimDotNet\Client\ClipMetadata.cs' 2026-02-24 21:16:25 -05:00
e34903b20f * 'Line 3: Removed the duplicate using directive for Newtonsoft.Json.Converters.' in file 'stream_deck_plugin\ClipTrimDotNet\Client\ClipMetadata.cs' 2026-02-24 21:16:18 -05:00
192c959d39 * 'Line 1: Ensure both 'Newtonsoft.Json' and 'Newtonsoft.Json.Converters' namespaces are included, as JsonPropertyAttribute and StringEnumConverter are defined in these namespaces. This resolves the CS0246 error for missing JsonPropertyAttribute.' in file 'stream_deck_plugin\ClipTrimDotNet\Client\ClipMetadata.cs' 2026-02-24 21:16:12 -05:00
60123d7450 * 'Line 37: Replaced [JsonProperty(PropertyName = path)] with [JsonPropertyName(path)] to use the System.Text.Json.Serialization.JsonPropertyNameAttribute, which is available in .NET 8 and the current project.
Line 40: Replaced [JsonProperty(PropertyName = index)] with [JsonPropertyName(index)] to use the correct attribute from System.Text.Json.Serialization.
Line 42: Replaced [JsonProperty(PropertyName = playtype)] with [JsonPropertyName(playtype)] to use the correct attribute from System.Text.Json.Serialization.
Line 45: Replaced [JsonProperty(PropertyName = volume)] with [JsonPropertyName(volume)] to use the correct attribute from System.Text.Json.Serialization.
Line 5: Replaced the using directive for Newtonsoft.Json with System.Text.Json.Serialization, as the project does not reference Newtonsoft.Json and should use System.Text.Json.Serialization.JsonPropertyNameAttribute instead.' in file 'stream_deck_plugin\ClipTrimDotNet\Player.cs'
2026-02-24 21:16:05 -05:00
8265951bd4 Move assembly metadata to .csproj and clean AssemblyInfo.cs
Assembly metadata such as title, product, copyright, and version information was moved from Properties/AssemblyInfo.cs to the ClipTrimDotNet.csproj file. The corresponding attributes and comments were removed from AssemblyInfo.cs, leaving only the trademark, culture, and GUID attributes. This streamlines project configuration and centralizes assembly information in the project file.
2026-02-24 21:15:31 -05:00
757d5ef1a7 Update package versions in ClipTrimDotNet.csproj
Upgraded several NuGet package dependencies in ClipTrimDotNet.csproj, including Microsoft.Extensions.* packages, System.* packages, and System.Drawing.Common, from version 8.x to 10.x or 9.x where applicable. This ensures compatibility with newer frameworks and libraries, and may provide bug fixes and performance improvements. No other files were modified.
2026-02-24 21:15:29 -05:00
d78c49d0ad Update ClipTrimDotNet.csproj dependencies to latest versions
Modernized the ClipTrimDotNet.csproj by upgrading several Microsoft.Extensions and System.* package references to their latest 8.x versions, replacing or removing older references. Added new CoreWCF packages and System.Configuration.ConfigurationManager for enhanced WCF and configuration support. Cleaned up unused or redundant references to streamline the project dependencies.
2026-02-24 21:14:40 -05:00
089023e7cf Refactor ClipTrimDotNet.csproj to SDK style and .NET 8
Converted the project file to the modern SDK-style format, targeting .NET 8.0 instead of .NET Framework 4.8. Replaced explicit assembly references and manual NuGet package management with <PackageReference> elements for all dependencies. Removed legacy MSBuild properties, imports, and targets, simplifying the project structure. Updated build events and ensured content files are copied as needed. This modernization improves maintainability and compatibility with current .NET tooling.
2026-02-24 21:14:28 -05:00
db97747f2e Commit upgrade plan 2026-02-24 21:13:55 -05:00
1e7141c43f some tests 2026-02-24 21:11:26 -05:00
8fda2a03af server playback 2026-02-24 19:08:27 -05:00
47cdaa76b6 python service managment on client, port configuration 2026-02-24 18:08:58 -05:00
d49ac95fa2 settings work 2026-02-22 14:57:04 -05:00
f2718282c7 Merge branch 'plugin_migration' into react_migration 2026-02-22 13:11:40 -05:00
86e30e6ec3 settings page created 2026-02-22 13:10:22 -05:00
b8f26496a0 skeleton of drag and drop 2026-02-21 21:15:06 -05:00
a761b81dd1 fully functional runtime stuff. Need settings then new features 2026-02-21 20:42:11 -05:00
c1948182ec remove .vs files 2026-02-21 11:21:11 -05:00
79 changed files with 3159 additions and 3541 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,34 @@
[ [
{ {
"name": "Uncategorized", "name": "Uncategorized",
"id": 0, "id": 0,
"clips": [] "clips": []
}, },
{ {
"name": "Test", "name": "Test",
"id": 1, "id": 1,
"clips": [] "clips": []
}, },
{ {
"name": "New", "name": "New",
"id": 2, "id": 2,
"clips": [ "clips": [
{ {
"endTime": 30, "endTime": 30,
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260220_193822.wav", "filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260220_193822.wav",
"name": "Pee pee\npoo poo", "name": "Pee pee\npoo poo",
"playbackType": "playStop", "playbackType": "playOverlap",
"startTime": 27.756510985786615, "startTime": 27.76674010920584,
"volume": 1 "volume": 0.25
}, },
{ {
"endTime": 28.597210828548004, "endTime": 27.516843118383072,
"filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260220_200442.wav", "filename": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\audio-service\\recordings\\audio_capture_20260220_200442.wav",
"name": "Clip 20260220_200442", "name": "Clip 20260220_200442",
"playbackType": "playStop", "playbackType": "playOverlap",
"startTime": 26.1853978671042, "startTime": 25.120307988450435,
"volume": 1 "volume": 0.64
} }
] ]
} }
] ]

View File

@ -1,7 +1,7 @@
sounddevice==0.5.1 sounddevice==0.5.1
numpy==1.22.3 numpy==1.22.3
python-osc==1.9.3 python-osc==1.9.3
scipy==1.10.1 scipy==1.10.1
comtypes==1.4.8 comtypes==1.4.8
pycaw==20240210 pycaw==20240210
Flask==3.1.2 Flask==3.1.2

View File

@ -1,10 +1,17 @@
{ {
"input_device": { "input_device": {
"default_samplerate": 44100.0, "channels": 2,
"index": 1, "default_samplerate": 48000,
"max_input_channels": 8, "index": 55,
"name": "VM Mic mix (VB-Audio Voicemeete" "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": {
"channels": 2,
"default_samplerate": 48000,
"index": 44,
"name": "VM to Headset (VB-Audio Voicemeeter VAIO)"
},
"http_port": 5010
} }

Binary file not shown.

View File

@ -0,0 +1,64 @@
import scipy.signal
import scipy.io.wavfile as wavfile
import numpy as np
import os
class AudioClip:
def __init__(self, metadata, target_sample_rate=44100):
"""
metadata: dict with keys 'filename', 'start', 'end' (seconds)
target_sample_rate: sample rate for playback
"""
self.metadata = metadata
self.file_path = metadata['filename']
self.start = metadata.get('startTime', 0)
self.end = metadata.get('endTime', None)
self.target_sample_rate = target_sample_rate
self.volume = metadata.get('volume', 1.0)
self.finished = False
self.audio_data, self.sample_rate = self._load_and_process_audio()
print(f"AudioClip created for {self.file_path} with start={self.start}s, end={self.end}s, sample_rate={self.sample_rate}Hz, length={len(self.audio_data)/self.sample_rate:.2f}s")
self.position = 0 # sample index for playback
def _load_and_process_audio(self):
# Load audio file
sample_rate, data = wavfile.read(self.file_path)
# Convert to float32
if data.dtype != np.float32:
data = data.astype(np.float32) / np.max(np.abs(data))
# Convert to mono if needed
if len(data.shape) > 1:
data = np.mean(data, axis=1)
# Resample if needed
if sample_rate != self.target_sample_rate:
num_samples = int(len(data) * self.target_sample_rate / sample_rate)
data = scipy.signal.resample(data, num_samples)
sample_rate = self.target_sample_rate
# Cache only the clip region
start_sample = int(self.start * sample_rate)
end_sample = int(self.end * sample_rate) if self.end else len(data)
cached = data[start_sample:end_sample]
cached *= self.volume # Apply volume
return cached, sample_rate
def get_samples(self, num_samples):
# Return next chunk for playback
if self.position >= len(self.audio_data):
self.finished = True
return np.zeros(num_samples, dtype=np.float32)
end_pos = min(self.position + num_samples, len(self.audio_data))
chunk = self.audio_data[self.position:end_pos]
self.position = end_pos
if self.position >= len(self.audio_data):
self.finished = True
# Pad if chunk is short
if len(chunk) < num_samples:
chunk = np.pad(chunk, (0, num_samples - len(chunk)), mode='constant')
return chunk
def is_finished(self):
return self.finished
def reset(self):
self.position = 0
self.finished = False

View File

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

View File

@ -1,156 +0,0 @@
import sounddevice as sd
import numpy as np
import os
from datetime import datetime
import scipy.io.wavfile as wavfile
from metadata_manager import MetaDataManager
class AudioRecorder:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
print("Creating new AudioRecorder instance")
cls._instance = super().__new__(cls)
cls._instance.init()
return cls._instance
def init(self):
"""
Initialize audio recorder with configurable parameters.
:param duration: Length of audio buffer in seconds
:param sample_rate: Audio sample rate (if None, use default device sample rate)
:param channels: Number of audio channels
"""
print(f"Initializing AudioRecorder")
self.duration = 30
self.sample_rate = 44100
self.channels = 2
self.buffer = np.zeros((int(self.duration * self.sample_rate), self.channels), dtype=np.float32)
self.recordings_dir = "recordings"
self.stream = sd.InputStream(
samplerate=self.sample_rate,
channels=self.channels,
callback=self.record_callback
)
def refresh_stream(self):
"""
Refresh the audio stream with updated parameters.
"""
was_active = self.stream.active
if was_active:
self.stream.stop()
self.buffer = np.zeros((int(self.duration * self.sample_rate), self.channels), dtype=np.float32)
self.stream = sd.InputStream(
samplerate=self.sample_rate,
channels=self.channels,
callback=self.record_callback
)
if was_active:
self.stream.start()
def record_callback(self, indata, frames, time, status):
"""
Circular buffer callback for continuous recording.
:param indata: Input audio data
:param frames: Number of frames
:param time: Timestamp
:param status: Recording status
"""
if status:
print(f"Recording status: {status}")
# Circular buffer implementation
self.buffer = np.roll(self.buffer, -frames, axis=0)
self.buffer[-frames:] = indata
def save_last_n_seconds(self):
"""
Save the last n seconds of audio to a file.
:param output_dir: Directory to save recordings
:return: Path to saved audio file
"""
# Create output directory if it doesn't exist
os.makedirs(self.recordings_dir, exist_ok=True)
# Generate filename with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = os.path.join(self.recordings_dir, f"audio_capture_{timestamp}.wav")
# Normalize audio to prevent clipping
audio_data = self.buffer / np.max(np.abs(self.buffer)) * .5
# Convert float32 to int16 for WAV file
audio_data_int16 = (audio_data * 32767).astype(np.int16)
# Write buffer to file
wavfile.write(filename, int(self.sample_rate), audio_data_int16)
meta = MetaDataManager()
clip_metadata = {
"filename": filename,
"name": f"Clip {timestamp}",
"playbackType":"playStop",
"volume": 1.0,
}
meta.add_clip_to_collection("Uncategorized", clip_metadata )
return clip_metadata
def set_buffer_duration(self, duration):
"""
Set the duration of the audio buffer.
:param duration: New buffer duration in seconds
"""
self.duration = duration
self.buffer = np.zeros((int(duration * self.sample_rate), self.channels), dtype=np.float32)
def set_recording_directory(self, directory):
"""
Set the directory where recordings will be saved.
:param directory: Path to the recordings directory
"""
self.recordings_dir = directory
def start_recording(self):
"""
Start continuous audio recording with circular buffer.
"""
if(self.stream.active):
print("Already recording")
return
print('number of channels', self.channels)
self.stream.start()
def stop_recording(self):
"""
Stop continuous audio recording with circular buffer.
"""
if(not self.stream.active):
print("Already stopped")
return
self.stream.stop()
def is_recording(self):
"""
Check if the audio stream is currently active.
:return: True if recording, False otherwise
"""
return self.stream.active

View File

@ -1,69 +1,85 @@
import argparse import argparse
import os import os
import sys import sys
from audio_recorder import AudioRecorder 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="*")
# 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(data):
type=int, io = AudioIO()
help='OSC server port number', io.save_last_n_seconds();
default=5010)
def main():
# Parse arguments # Create argument parser
args = parser.parse_args() parser = argparse.ArgumentParser(description='Audio Recording Service')
audio_manager = WindowsAudioManager()
settings = SettingsManager() # OSC port argument
parser.add_argument('--osc-port',
# Ensure save path exists type=int,
os.makedirs(settings.get_settings('save_path'), exist_ok=True) help='OSC server port number',
default=5010)
# Register blueprints # Parse arguments
app.register_blueprint(recording_bp) args = parser.parse_args()
app.register_blueprint(device_bp) audio_manager = WindowsAudioManager()
app.register_blueprint(metadata_bp) settings = SettingsManager()
app.register_blueprint(settings_bp) meta = MetaDataManager()
app.run(host='127.0.0.1', port=args.osc_port, debug=False, use_reloader=True)
# socketio.run(app, host='127.0.0.1', port=args.osc_port, debug=False, use_reloader=True)
# Ensure save path exists
os.makedirs(settings.get_settings('save_path'), exist_ok=True)
# Run the OSC server
try:
print(f"Starting OSC Recording Server on port {args.osc_port}") io = AudioIO()
io.start_recording()
# settings.socket = socketio
# osc_server.run_server() io.socket = socketio
except KeyboardInterrupt: meta.socket = socketio
print("\nServer stopped by user.")
except Exception as e: # Register blueprints
print(f"Error starting server: {e}") app.register_blueprint(recording_bp)
sys.exit(1) app.register_blueprint(device_bp)
app.register_blueprint(metadata_bp)
app.register_blueprint(settings_bp)
if __name__ == "__main__": # 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)
# 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,103 @@
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)
self.save_metadata()
def remove_clip_from_collection(self, collection_name, clip_metadata):
collection = next((c for c in self.collections if c.get("name") == collection_name), None) def remove_clip_from_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:
# Remove all clips with the same file name as clip_metadata["file_name"] raise ValueError(f"Collection '{collection_name}' does not exist.")
in_list = any(clip.get("filename") == clip_metadata.get("filename") for clip in collection["clips"]) # Remove all clips with the same file name as clip_metadata["file_name"]
if not in_list: in_list = any(clip.get("filename") == clip_metadata.get("filename") for clip in collection["clips"])
raise ValueError(f"Clip with filename '{clip_metadata.get('filename')}' not found in collection '{collection_name}'.") if not in_list:
raise ValueError(f"Clip with filename '{clip_metadata.get('filename')}' not found in collection '{collection_name}'.")
collection["clips"] = [
clip for clip in collection["clips"] collection["clips"] = [
if clip.get("filename") != clip_metadata.get("filename") clip for clip in collection["clips"]
] if clip.get("filename") != clip_metadata.get("filename")
self.save_metadata() ]
self.save_metadata()
def move_clip_to_collection(self, source_collection, target_collection, clip_metadata):
self.remove_clip_from_collection(source_collection, clip_metadata) def move_clip_to_collection(self, source_collection, target_collection, clip_metadata):
self.add_clip_to_collection(target_collection, clip_metadata) self.remove_clip_from_collection(source_collection, clip_metadata)
self.add_clip_to_collection(target_collection, clip_metadata)
def edit_clip_in_collection(self, collection_name, new_clip_metadata):
collection = next((c for c in self.collections if c.get("name") == collection_name), None) def edit_clip_in_collection(self, collection_name, new_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:
# Find the index of the clip with the same file name as old_clip_metadata["file_name"] raise ValueError(f"Collection '{collection_name}' does not exist.")
index = next((i for i, clip in enumerate(collection["clips"]) if clip.get("filename") == new_clip_metadata.get("filename")), None) # Find the index of the clip with the same file name as old_clip_metadata["file_name"]
if index is None: index = next((i for i, clip in enumerate(collection["clips"]) if clip.get("filename") == new_clip_metadata.get("filename")), None)
raise ValueError(f"Clip with filename '{new_clip_metadata.get('filename')}' not found in collection '{collection_name}'.") if index is None:
raise ValueError(f"Clip with filename '{new_clip_metadata.get('filename')}' not found in collection '{collection_name}'.")
collection["clips"][index] = new_clip_metadata
self.save_metadata() collection["clips"][index] = new_clip_metadata
self.save_metadata()
def get_collections(self):
return list(map(lambda c: {"name": c.get("name"), "id": c.get("id")}, self.collections)) def get_collections(self):
return list(map(lambda c: {"name": c.get("name"), "id": c.get("id")}, self.collections))
def get_clips_in_collection(self, collection_name):
collection = next((c for c in self.collections if c.get("name") == collection_name), None) def get_clips_in_collection(self, collection_name):
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:
return collection["clips"] raise ValueError(f"Collection '{collection_name}' does not exist.")
return collection["clips"]
def reorder_clips_in_collection(self, collection_name, new_order):
collection = next((c for c in self.collections if c.get("name") == collection_name), None) def reorder_clips_in_collection(self, collection_name, new_order):
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:
existing_filenames = {clip.get("filename") for clip in collection["clips"]} raise ValueError(f"Collection '{collection_name}' does not exist.")
new_filenames = {clip.get("filename") for clip in new_order} 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.") if not new_filenames.issubset(existing_filenames):
raise ValueError("New order contains clips that do not exist in the collection.")
collection["clips"] = new_order
self.save_metadata() collection["clips"] = new_order
if not self.socket is None:
def save_metadata(self): self.socket.emit('collection_updated', collection)
with open(self.metadata_file, "w") as f: self.save_metadata()
json.dump(self.collections, f, indent=4)
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

@ -1,11 +1,11 @@
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
from windows_audio import WindowsAudioManager from windows_audio import WindowsAudioManager
from audio_recorder import AudioRecorder from audio_io import AudioIO
device_bp = Blueprint('device', __name__) device_bp = Blueprint('device', __name__)
audio_manager = WindowsAudioManager() audio_manager = WindowsAudioManager()
recorder = AudioRecorder() recorder = AudioIO()
# @device_bp.route('/device/set', methods=['POST']) # @device_bp.route('/device/set', methods=['POST'])
# def set_audio_device(): # def set_audio_device():
@ -19,13 +19,13 @@ recorder = AudioRecorder()
# except Exception as e: # except Exception as e:
# return jsonify({'status': 'error', 'message': str(e)}), 400 # return jsonify({'status': 'error', 'message': str(e)}), 400
@device_bp.route('/device/get', methods=['GET']) # @device_bp.route('/device/get', methods=['GET'])
def get_audio_device(): # def get_audio_device():
try: # try:
device_info = audio_manager.get_default_device('input') # device_info = audio_manager.get_default_device('input')
return jsonify({'status': 'success', 'device_info': device_info}) # return jsonify({'status': 'success', 'device_info': device_info})
except Exception as e: # except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 400 # return jsonify({'status': 'error', 'message': str(e)}), 400
@device_bp.route('/device/list', methods=['GET']) @device_bp.route('/device/list', methods=['GET'])
def list_audio_devices(): def list_audio_devices():

View File

@ -91,10 +91,16 @@ def edit_clip_in_collection():
meta_manager = MetaDataManager() meta_manager = MetaDataManager()
collection_name = request.json.get('name') collection_name = request.json.get('name')
clip_metadata = request.json.get('clip') clip_metadata = request.json.get('clip')
print(f"Received request to edit clip in collection '{collection_name}': {clip_metadata}") # print(f"Received request to edit clip in collection '{collection_name}': {clip_metadata}")
try: try:
meta_manager.edit_clip_in_collection(collection_name, clip_metadata) meta_manager.edit_clip_in_collection(collection_name, clip_metadata)
collections = meta_manager.collections collections = meta_manager.collections
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,53 +1,57 @@
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
from audio_recorder import AudioRecorder 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 = AudioRecorder() 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 = AudioRecorder() 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 = AudioRecorder() 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 = AudioRecorder() 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('HTTP: Starting audio playback') print(f"Playing clip")
try: # print('HTTP: Starting audio playback')
# os.remove(filename) clip = request.json
return jsonify({'status': 'success'}) try:
except Exception as e: io = AudioIO()
io.play_clip(clip)
# os.remove(filename)
return jsonify({'status': 'success'})
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 400 return jsonify({'status': 'error', 'message': str(e)}), 400

View File

@ -1,25 +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/<name>', methods=['POST']) @settings_bp.route('/settings/update', methods=['POST'])
def set_setting(name): def set_all_settings():
value = request.json.get('value') settings = request.json.get('settings')
if value is None: print (f"Received settings update: {settings}")
return jsonify({'status': 'error', 'message': 'Value is required'}), 400 if settings is None:
SettingsManager().set_settings(name, value) return jsonify({'status': 'error', 'message': 'Settings are required'}), 400
return jsonify({'status': 'success', 'name': name, 'value': value}) try:
for name, value in settings.items():
print(f"Updating setting '{name}' to '{value}'")
SettingsManager().set_settings(name, value)
return jsonify({'status': 'success', 'settings': settings})
except ValueError as e:
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,6 +1,6 @@
import os import os
import json import json
from audio_recorder import AudioRecorder from audio_io import AudioIO
from windows_audio import WindowsAudioManager from windows_audio import WindowsAudioManager
class SettingsManager: class SettingsManager:
@ -13,6 +13,7 @@ class SettingsManager:
return cls._instance return cls._instance
def init(self): def init(self):
# read settings file from executing directory # read settings file from executing directory
print("Initializing SettingsManager", os.getcwd())
self.settings_file = os.path.join(os.getcwd(), "settings.json") self.settings_file = os.path.join(os.getcwd(), "settings.json")
if os.path.exists(self.settings_file): if os.path.exists(self.settings_file):
with open(self.settings_file, "r") as f: with open(self.settings_file, "r") as f:
@ -20,21 +21,49 @@ class SettingsManager:
else: else:
self.settings = { self.settings = {
"input_device": None, "input_device": None,
"output_device": None,
"save_path": os.path.join(os.getcwd(), "recordings"), "save_path": os.path.join(os.getcwd(), "recordings"),
"recording_length": 15 "recording_length": 15
} }
audio_manager = WindowsAudioManager() audio_manager = WindowsAudioManager()
devices = audio_manager.list_audio_devices('input') input_devices = audio_manager.list_audio_devices('input')
print(f"Available input devices: {self.settings}") output_devices = audio_manager.list_audio_devices('output')
input = self.settings["input_device"] # print("Available input devices:")
# for i, dev in enumerate(input_devices):
# print(i, dev['name'])
# print("Available output devices:")
# for i, dev in enumerate(output_devices):
# print(i, dev['name'])
# print(f"Available input devices: {input_devices}")
# print(f"Available output devices: {output_devices}")
input = None
output = None
if("input_device" in self.settings):
input = self.settings["input_device"]
if("output_device" in self.settings):
output = self.settings["output_device"]
#see if input device is in "devices", if not set to the first index #see if input device is in "devices", if not set to the first index
if input is not None and any(d['name'] == input["name"] for d in devices): if input is not None and any(d['name'] == input["name"] for d in input_devices):
print(f"Using saved input device index: {input}") # print(f"Using saved input device index: {input}")
pass
else: else:
input = devices[0] if devices else None input = input_devices[0] if input_devices else None
self.settings["input_device"] = input self.settings["input_device"] = input
#see if output device is in "devices", if not set to the first index
if output is not None and any(d['name'] == output["name"] for d in output_devices):
# print(f"Using saved output device index: {output}")
pass
else:
output = output_devices[0] if output_devices else None
self.settings["output_device"] = output
if not "http_port" in self.settings:
self.settings["http_port"] = 5010
self.save_settings() self.save_settings()
@ -48,6 +77,8 @@ class SettingsManager:
return self.settings return self.settings
def set_settings(self, name, value): def set_settings(self, name, value):
if(name not in self.settings):
raise ValueError(f"Setting '{name}' not found.")
self.settings[name] = value self.settings[name] = value
self.save_settings() self.save_settings()
@ -57,13 +88,14 @@ class SettingsManager:
json.dump(self.settings, f, indent=4) json.dump(self.settings, f, indent=4)
def refresh_settings(self): def refresh_settings(self):
recorder = AudioRecorder() recorder = AudioIO()
# Update recorder parameters based on new setting # Update recorder parameters based on new setting
recorder.set_buffer_duration(self.get_settings('recording_length')) recorder.set_buffer_duration(self.get_settings('recording_length'))
recorder.recordings_dir = self.get_settings('save_path') recorder.recordings_dir = self.get_settings('save_path')
audio_manager = WindowsAudioManager() audio_manager = WindowsAudioManager()
audio_manager.set_default_input_device(self.get_settings('input_device')['index']) audio_manager.set_default_input_device(self.get_settings('input_device')['index'])
audio_manager.set_default_output_device(self.get_settings('output_device')['index'])
recorder.refresh_stream() recorder.refresh_streams()

View File

@ -1,123 +1,112 @@
import sounddevice as sd import sounddevice as sd
import numpy as np import numpy as np
import comtypes import comtypes
import comtypes.client import comtypes.client
from comtypes import CLSCTX_ALL from comtypes import CLSCTX_ALL
from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume
import json import json
class WindowsAudioManager: class WindowsAudioManager:
_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):
""" """
Initialize Windows audio device and volume management. Initialize Windows audio device and volume management.
""" """
self.devices = sd.query_devices() host_apis = sd.query_hostapis()
self.default_input = sd.default.device[0] wasapi_device_indexes = None
self.default_output = sd.default.device[1] for api in host_apis:
if api['name'].lower() == 'Windows WASAPI'.lower():
def list_audio_devices(self, kind='input'): wasapi_device_indexes = api['devices']
""" break
List available audio devices. # print(f"Host APIs: {host_apis}")
# print(f"WASAPI Device Indexes: {wasapi_device_indexes}")
:param kind: 'input' or 'output' wasapi_device_indexes = set(wasapi_device_indexes) if wasapi_device_indexes is not None else set()
:return: List of audio devices self.devices = [dev for dev in sd.query_devices() if dev['index'] in wasapi_device_indexes]
""" # self.devices = sd.query_devices()
if kind == 'input': # print(f"devices: {self.devices}")
return [
{ self.default_input = sd.default.device[0]
'index': dev['index'], self.default_output = sd.default.device[1]
'name': dev['name'],
'max_input_channels': dev['max_input_channels'], def list_audio_devices(self, kind='input'):
'default_samplerate': dev['default_samplerate'] """
} List available audio devices.
for dev in self.devices if dev['max_input_channels'] > 0
] :param kind: 'input' or 'output'
elif kind == 'output': :return: List of audio devices
return [ """
{ if kind == 'input':
'index': dev['index'], return [
'name': dev['name'], {
'max_output_channels': dev['max_output_channels'], 'index': dev['index'],
'default_samplerate': dev['default_samplerate'] 'name': dev['name'],
} 'channels': dev['max_input_channels'],
for dev in self.devices if dev['max_output_channels'] > 0 'default_samplerate': dev['default_samplerate']
] }
def get_default_device(self, kind='input'): for dev in self.devices if dev['max_input_channels'] > 0
""" ]
Get the default audio device. elif kind == 'output':
return [
:param kind: 'input' or 'output' {
:return: Default audio device information 'index': dev['index'],
""" 'name': dev['name'],
if kind == 'input': 'channels': dev['max_output_channels'],
dev = self.devices[self.default_input] 'default_samplerate': dev['default_samplerate']
return [ }
{ for dev in self.devices if dev['max_output_channels'] > 0
'index': dev['index'], ]
'name': dev['name'], def get_default_device(self, kind='input'):
'max_input_channels': dev['max_input_channels'], """
'default_samplerate': dev['default_samplerate'] Get the default audio device.
}
] :param kind: 'input' or 'output'
:return: Default audio device information
def set_default_input_device(self, device_index): """
if(device_index is None): if kind == 'input':
return self.get_current_input_device_sample_rate() dev = self.devices[self.default_input]
""" return [
Set the default input audio device. {
'index': dev['index'],
:param device_index: Index of the audio device 'name': dev['name'],
:return: Sample rate of the selected device 'max_input_channels': dev['max_input_channels'],
""" 'default_samplerate': dev['default_samplerate']
sd.default.device[0] = device_index }
self.default_input = device_index ]
# Get the sample rate of the selected device def set_default_input_device(self, device_index):
device_info = sd.query_devices(device_index) if(device_index is None):
return device_info['default_samplerate'] return 0
"""
def get_current_input_device_sample_rate(self): Set the default input audio device.
"""
Get the sample rate of the current input device. :param device_index: Index of the audio device
:return: Sample rate of the selected device
:return: Sample rate of the current input device """
""" sd.default.device[0] = device_index
device_info = sd.query_devices(self.default_input) self.default_input = device_index
return device_info['default_samplerate']
# Get the sample rate of the selected device
def get_system_volume(self): device_info = sd.query_devices(device_index)
""" return device_info['default_samplerate']
Get the system master volume.
def set_default_output_device(self, device_index):
:return: Current system volume (0.0 to 1.0) if(device_index is None):
""" return self.get_current_output_device_sample_rate()
devices = AudioUtilities.GetSpeakers() """
interface = devices.Activate( Set the default output audio device.
IAudioEndpointVolume._iid_,
CLSCTX_ALL, :param device_index: Index of the audio device
None :return: Sample rate of the selected device
) """
volume = interface.QueryInterface(IAudioEndpointVolume) sd.default.device[1] = device_index
return volume.GetMasterVolumeLevelScalar() self.default_output = device_index
def set_system_volume(self, volume_level): # Get the sample rate of the selected device
""" device_info = sd.query_devices(device_index)
Set the system master volume. return device_info['default_samplerate']
:param volume_level: Volume level (0.0 to 1.0)
"""
devices = AudioUtilities.GetSpeakers()
interface = devices.Activate(
IAudioEndpointVolume._iid_,
CLSCTX_ALL,
None
)
volume = interface.QueryInterface(IAudioEndpointVolume)
volume.SetMasterVolumeLevelScalar(volume_level, None)

16
electron-ui/settings.json Normal file
View File

@ -0,0 +1,16 @@
{
"input_device": {
"index": 49,
"name": "Microphone (Logi C615 HD WebCam)",
"channels": 1,
"default_samplerate": 48000.0
},
"output_device": {
"index": 40,
"name": "Speakers (Realtek(R) Audio)",
"channels": 2,
"default_samplerate": 48000.0
},
"save_path": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\electron-ui\\recordings",
"recording_length": 15
}

View File

@ -1,5 +1,7 @@
const AudioChannels = { const AudioChannels = {
LOAD_AUDIO_BUFFER: 'audio:loadAudioBuffer', LOAD_AUDIO_BUFFER: 'audio:loadAudioBuffer',
GET_PORT: 'audio:getPort',
RESTART_SERVICE: 'audio:restartService',
} as const; } as const;
export default AudioChannels; export default AudioChannels;

View File

@ -2,6 +2,7 @@ import { ipcMain } from 'electron';
import fs from 'fs'; import fs from 'fs';
import AudioChannels from './channels'; import AudioChannels from './channels';
import { LoadAudioBufferArgs, LoadAudioBufferResult } from './types'; import { LoadAudioBufferArgs, LoadAudioBufferResult } from './types';
import PythonSubprocessManager from '../../main/service';
export default function registerAudioIpcHandlers() { export default function registerAudioIpcHandlers() {
ipcMain.handle( ipcMain.handle(
@ -15,4 +16,25 @@ export default function registerAudioIpcHandlers() {
} }
}, },
); );
ipcMain.handle(AudioChannels.GET_PORT, async () => {
try {
if (PythonSubprocessManager.instance?.portNumber) {
return { port: PythonSubprocessManager.instance.portNumber };
}
return { error: 'Port number not available yet.' };
} catch (err: any) {
return { error: err.message };
}
});
ipcMain.handle(AudioChannels.RESTART_SERVICE, async () => {
try {
PythonSubprocessManager.instance?.restart();
return { success: true };
} catch (err: any) {
return { success: false, error: err.message };
}
});
} }

View File

@ -6,3 +6,22 @@ export interface LoadAudioBufferResult {
buffer?: Buffer; buffer?: Buffer;
error?: string; error?: string;
} }
export interface GetPortResult {
port?: number;
error?: string;
}
export interface SetPortArgs {
port: number;
}
export interface SetPortResult {
success: boolean;
error?: string;
}
export interface RestartServiceResult {
success: boolean;
error?: string;
}

View File

@ -16,6 +16,7 @@ import log from 'electron-log';
import MenuBuilder from './menu'; import MenuBuilder from './menu';
import { resolveHtmlPath } from './util'; import { resolveHtmlPath } from './util';
import registerFileIpcHandlers from '../ipc/audio/main'; import registerFileIpcHandlers from '../ipc/audio/main';
import PythonSubprocessManager from './service';
class AppUpdater { class AppUpdater {
constructor() { constructor() {
@ -110,6 +111,10 @@ 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();

View File

@ -1,8 +1,7 @@
// Disable no-unused-vars, broken for spread args // Disable no-unused-vars, broken for spread args
/* eslint no-unused-vars: off */ /* eslint no-unused-vars: off */
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';
import FileChannels from '../ipc/audio/channels'; import { LoadAudioBufferArgs } from '../ipc/audio/types';
import { LoadAudioBufferArgs, ReadTextArgs } from '../ipc/audio/types';
import AudioChannels from '../ipc/audio/channels'; import AudioChannels from '../ipc/audio/channels';
// import '../ipc/file/preload'; // Import file API preload to ensure it runs and exposes the API // import '../ipc/file/preload'; // Import file API preload to ensure it runs and exposes the API
@ -41,10 +40,8 @@ const audioHandler = {
filePath, filePath,
} satisfies LoadAudioBufferArgs), } satisfies LoadAudioBufferArgs),
readText: (filePath: string) => getPort: () => ipcRenderer.invoke(AudioChannels.GET_PORT),
ipcRenderer.invoke(AudioChannels.READ_TEXT, { restartService: () => ipcRenderer.invoke(AudioChannels.RESTART_SERVICE),
filePath,
} satisfies ReadTextArgs),
}; };
contextBridge.exposeInMainWorld('audio', audioHandler); contextBridge.exposeInMainWorld('audio', audioHandler);

View File

@ -0,0 +1,79 @@
import { spawn, ChildProcessWithoutNullStreams } from 'child_process';
import path from 'path';
export default class PythonSubprocessManager {
// eslint-disable-next-line no-use-before-define
public static instance: PythonSubprocessManager | null = null;
private process: ChildProcessWithoutNullStreams | null = null;
private scriptPath: string;
private working_dir: string = path.join(
__dirname,
'..',
'..',
'..',
'audio-service',
);
public portNumber: number | null = null;
constructor(scriptPath: string) {
this.scriptPath = scriptPath;
PythonSubprocessManager.instance = this;
}
start(args: string[] = []): void {
if (this.process) {
throw new Error('Process already running.');
}
console.log(`Using Python working directory at: ${this.working_dir}`);
console.log(`Starting Python subprocess with script: ${this.scriptPath}`);
this.process = spawn(
'venv/Scripts/python.exe',
[this.scriptPath, ...args],
{
cwd: this.working_dir,
detached: false,
stdio: 'pipe',
},
);
this.process.stdout.on('data', (data: Buffer) => {
console.log(`Python stdout: ${data.toString()}`);
});
this.process.stderr.on('data', (data: Buffer) => {
// console.error(`Python stderr: ${data.toString()}`);
const lines = data.toString().split('\n');
// eslint-disable-next-line no-restricted-syntax
for (const line of lines) {
const match = line.match(/Running on .*:(\d+)/);
if (match) {
const port = parseInt(match[1], 10);
console.log(`Detected port: ${port}`);
this.portNumber = port;
}
}
});
this.process.on('exit', () => {
console.log('Python subprocess exited.');
this.process = null;
});
}
stop(): void {
if (this.process) {
this.process.kill();
this.process = null;
}
}
restart(args: string[] = []): void {
this.stop();
this.start(args);
}
isHealthy(): boolean {
return !!this.process && !this.process.killed;
}
}

View File

@ -2,6 +2,8 @@ import { MemoryRouter as Router, Routes, Route } from 'react-router-dom';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import Dialog from '@mui/material/Dialog'; import Dialog from '@mui/material/Dialog';
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';
@ -10,6 +12,9 @@ import './App.css';
import ClipList from './components/ClipList'; import ClipList from './components/ClipList';
import { useAppDispatch, useAppSelector } from './hooks'; import { useAppDispatch, useAppSelector } from './hooks';
import { store } from '../redux/main'; import { store } from '../redux/main';
import { useNavigate } from 'react-router-dom';
import SettingsPage from './Settings';
import apiFetch from './api';
function MainPage() { function MainPage() {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
@ -21,11 +26,12 @@ 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();
useEffect(() => { useEffect(() => {
const fetchMetadata = async () => { const fetchMetadata = async () => {
try { try {
const response = await fetch('http://localhost:5010/meta'); const response = await apiFetch('meta');
const data = await response.json(); const data = await response.json();
dispatch({ type: 'metadata/setAllData', payload: data }); dispatch({ type: 'metadata/setAllData', payload: data });
} catch (error) { } catch (error) {
@ -137,6 +143,22 @@ function MainPage() {
</li> </li>
))} ))}
</ul> </ul>
{/* Settings Button at Bottom Left */}
<div className="mt-auto mb-2">
<button
type="button"
className="w-full rounded px-4 py-2 bg-neutral-800 text-offwhite hover:bg-plumDark text-left"
style={{
position: 'absolute',
bottom: 16,
left: 8,
width: 'calc(100% - 16px)',
}}
onClick={() => navigate('/settings')}
>
Settings
</button>
</div>
</nav> </nav>
{/* Main Content */} {/* Main Content */}
<div <div
@ -150,13 +172,39 @@ function MainPage() {
} }
export default function App() { export default function App() {
const theme = createTheme({
colorSchemes: {
light: false,
dark: {
palette: {
primary: {
main: '#6e44ba', // plum
dark: '#6e44ba', // plum
contrastText: '#ffffff',
},
secondary: {
main: '#4f3186', // plumDark
dark: '#4f3186', // plumDark
contrastText: '#ffffff',
},
},
},
},
// colorSchemes: {
// light: false,
// dark: true,
// },
});
return ( return (
<Provider store={store}> <Provider store={store}>
<Router> <ThemeProvider theme={theme}>
<Routes> <Router>
<Route path="/" element={<MainPage />} /> <Routes>
</Routes> <Route path="/" element={<MainPage />} />
</Router> <Route path="/settings" element={<SettingsPage />} />
</Routes>
</Router>
</ThemeProvider>
</Provider> </Provider>
); );
} }

View File

@ -0,0 +1,274 @@
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import './App.css';
import TextField from '@mui/material/TextField';
import Select from '@mui/material/Select';
import MenuItem from '@mui/material/MenuItem';
import apiFetch from './api';
type AudioDevice = {
index: number;
name: string;
default_sample_rate: number;
channels: number;
};
type Settings = {
http_port: number;
input_device: AudioDevice;
output_device: AudioDevice;
recording_length: number;
save_path: string;
};
const defaultSettings: Settings = {
http_port: 0,
input_device: { index: 0, name: '', default_sample_rate: 0, channels: 0 },
output_device: { index: 0, name: '', default_sample_rate: 0, channels: 0 },
recording_length: 0,
save_path: '',
};
async function fetchAudioDevices(
type: 'input' | 'output',
): Promise<AudioDevice[]> {
// Replace with actual backend call
// Example: return window.api.getAudioDevices();
return apiFetch(`device/list?device_type=${type}`)
.then((res) => res.json())
.then((data) => data.devices as AudioDevice[])
.catch((error) => {
console.error('Error fetching audio devices:', error);
return [];
});
}
async function fetchSettings(): Promise<Settings> {
// Replace with actual backend call
// Example: return window.api.getAudioDevices();
console.log('Fetching settings from backend...');
return apiFetch('settings')
.then((res) => res.json())
.then((data) => data.settings as Settings)
.catch((error) => {
console.error('Error fetching settings:', error);
return defaultSettings;
});
}
const sendSettingsToBackend = async (settings: Settings) => {
// Replace with actual backend call
// Example: window.api.updateSettings(settings);
console.log('Settings updated:', settings);
await apiFetch('settings/update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ settings }),
})
.then((res) => res.json())
.then((data) => {
console.log('Settings update response:', data);
if (data.status === 'success') {
window.audio.restartService();
}
return data;
})
.catch((error) => {
console.error('Error updating settings:', error);
});
};
export default function SettingsPage() {
const [settings, setSettings] = useState<Settings>(defaultSettings);
const [inputDevices, setInputDevices] = useState<AudioDevice[]>([]);
const [outputDevices, setOutputDevices] = useState<AudioDevice[]>([]);
const navigate = useNavigate();
useEffect(() => {
fetchSettings()
.then((fetchedSettings) => {
console.log('Fetched settings:', fetchedSettings);
setSettings(fetchedSettings);
return null;
})
.then(() => {
return fetchAudioDevices('input');
})
.then((devices) => {
setInputDevices(devices);
// console.log('Input devices:', devices);
return fetchAudioDevices('output');
})
.then((devices) => {
setOutputDevices(devices);
// console.log('Output devices:', devices);
return devices;
})
.catch((error) => {
console.error('Error fetching audio devices:', error);
});
}, []);
useEffect(() => {}, [settings]);
const handleChange = () => {
sendSettingsToBackend(settings);
// const { name, value } = e.target;
// setSettings((prev) => ({
// ...prev,
// [name]: value,
// }));
};
const handleFolderChange = async () => {
// Replace with actual folder picker
// Example: const folder = await window.api.selectFolder();
// const folder = window.prompt(
// 'Enter output folder path:',
// settings.outputFolder,
// );
// if (folder !== null) {
// setSettings((prev) => ({
// ...prev,
// outputFolder: folder,
// }));
// }
};
return (
<div className="min-w-screen min-h-screen bg-midnight text-offwhite flex items-center justify-center relative">
<div className="w-3/4 min-w-[600px] max-w-[800px] self-start flex flex-col font-sans bg-midnight text-offwhite p-6 rounded-lg relative">
{/* X Close Button */}
<button
type="button"
className="absolute top-6 right-6 text-3xl font-bold text-offwhite bg-transparent hover:text-plumDark"
aria-label="Close settings"
onClick={() => navigate('/')}
>
×
</button>
<span className="text-2xl font-bold mb-4">Settings</span>
<div className="mb-4 flex justify-between">
<span>HTTP Port:</span>
<TextField
variant="standard"
type="text"
name="httpPort"
value={settings.http_port}
onBlur={() => handleChange()}
onChange={(e) => {
if (!Number.isNaN(Number(e.target.value))) {
setSettings((prev) => ({
...prev,
http_port: Number(e.target.value),
}));
}
}}
className="ml-2 text-white w-[150px]"
/>
</div>
<div className="mb-4 flex justify-between">
<span>Input Audio Device:</span>
<Select
variant="standard"
name="inputDevice"
value={settings.input_device.index}
onChange={(e) => {
const newDevice = inputDevices.find(
(dev) => dev.index === Number(e.target.value),
);
console.log('Selected input device index:', newDevice);
if (newDevice) {
setSettings((prev) => ({
...prev,
input_device: newDevice,
}));
sendSettingsToBackend({
...settings,
input_device: newDevice,
});
}
}}
className="ml-2 w-64"
>
{inputDevices.map((dev) => (
<MenuItem key={dev.index} value={dev.index}>
{dev.name}
</MenuItem>
))}
</Select>
</div>
<div className="mb-4 flex justify-between">
<span>Output Audio Device:</span>
<Select
variant="standard"
name="outputDevice"
value={settings.output_device.index}
onChange={(e) => {
const newDevice = outputDevices.find(
(dev) => dev.index === Number(e.target.value),
);
if (newDevice) {
setSettings((prev) => ({
...prev,
output_device: newDevice,
}));
sendSettingsToBackend({
...settings,
output_device: newDevice,
});
}
}}
className="ml-2 w-64"
>
{outputDevices.map((dev) => (
<MenuItem key={dev.index} value={dev.index}>
{dev.name}
</MenuItem>
))}
</Select>
</div>
<div className="mb-4 flex justify-between">
<span>Recording Length (seconds):</span>
<TextField
variant="standard"
type="text"
name="recordingLength"
value={settings.recording_length}
onChange={(e) => {
if (!Number.isNaN(Number(e.target.value))) {
setSettings((prev) => ({
...prev,
recording_length: Number(e.target.value),
}));
}
}}
onBlur={() => handleChange()}
className="ml-2 w-[150px]"
/>
</div>
<div className="mb-4 flex justify-between">
<span>Clip Output Folder:</span>
<div className="flex justify-end">
<TextField
variant="standard"
type="text"
name="savePath"
value={settings.save_path}
className="ml-2 w-[300px]"
/>
<button
type="button"
onClick={handleFolderChange}
className="ml-2 px-3 py-1 rounded bg-plumDark text-offwhite hover:bg-plum"
>
...
</button>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,13 @@
const getBaseUrl = async () => {
const port = await window.audio.getPort();
if (port.error || !port.port) {
return `http://localhost:5010`;
}
// You can store the base URL in localStorage, a config file, or state
return `http://localhost:${port.port}`;
};
export default async function apiFetch(endpoint: string, options = {}) {
const url = `${await getBaseUrl()}/${endpoint}`;
return fetch(url, options);
}

View File

@ -9,6 +9,9 @@ import Dialog from '@mui/material/Dialog';
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 Slider from '@mui/material/Slider';
import ToggleButton from '@mui/material/ToggleButton';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import { useWavesurfer } from '@wavesurfer/react'; import { useWavesurfer } from '@wavesurfer/react';
import RegionsPlugin from 'wavesurfer.js/dist/plugins/regions.esm.js'; import RegionsPlugin from 'wavesurfer.js/dist/plugins/regions.esm.js';
import ZoomPlugin from 'wavesurfer.js/dist/plugins/zoom.esm.js'; import ZoomPlugin from 'wavesurfer.js/dist/plugins/zoom.esm.js';
@ -19,8 +22,10 @@ import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import { useSortable } from '@dnd-kit/sortable'; import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities'; import { CSS } from '@dnd-kit/utilities';
import { ClipMetadata } from '../../redux/types'; import { ClipMetadata, PlaybackType } from '../../redux/types';
import { useAppSelector } from '../hooks'; import { useAppSelector } from '../hooks';
import PlayStopIcon from './playStopIcon';
import PlayOverlapIcon from './playOverlapIcon';
export interface AudioTrimmerProps { export interface AudioTrimmerProps {
metadata: ClipMetadata; metadata: ClipMetadata;
@ -43,6 +48,7 @@ export default function AudioTrimmer({
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [dropdownOpen, setDropdownOpen] = useState(false); const [dropdownOpen, setDropdownOpen] = useState(false);
const [nameInput, setNameInput] = useState<string>(metadata.name); const [nameInput, setNameInput] = useState<string>(metadata.name);
const [volumeInput, setVolumeInput] = useState<number>(metadata.volume ?? 1);
const collectionNames = useAppSelector((state) => const collectionNames = useAppSelector((state) =>
state.collections.map((col) => col.name), state.collections.map((col) => col.name),
); );
@ -223,6 +229,7 @@ export default function AudioTrimmer({
} else { } else {
const allRegions = (plugins[0] as RegionsPlugin).getRegions(); const allRegions = (plugins[0] as RegionsPlugin).getRegions();
if (allRegions.length > 0) { if (allRegions.length > 0) {
wavesurfer.setVolume(metadata.volume ?? 1);
wavesurfer.play(allRegions[0].start, allRegions[0].end); wavesurfer.play(allRegions[0].start, allRegions[0].end);
} else { } else {
wavesurfer.play(); wavesurfer.play();
@ -401,12 +408,74 @@ export default function AudioTrimmer({
<div className="m-1 wavesurfer-scroll-container"> <div className="m-1 wavesurfer-scroll-container">
<div ref={containerRef} className="wavesurfer-inner" /> <div ref={containerRef} className="wavesurfer-inner" />
</div> </div>
<div className="grid justify-items-stretch grid-cols-2 text-neutral-500"> <div className="flex justify-between mt-2">
<div className="m-1 flex justify-start"> <span className="w-1/5 flex-none text-sm text-neutral-500 self-center">
<text className="text-sm "> Clip: {formatTime(metadata.startTime ?? 0)} -{' '}
Clip: {formatTime(metadata.startTime ?? 0)} -{' '} {formatTime(metadata.endTime ?? 0)}
{formatTime(metadata.endTime ?? 0)} </span>
</text> <div className="w-3/5 flex-1 flex justify-center items-center">
<Slider
value={volumeInput}
min={0}
max={1}
step={0.01}
onChange={(e, newValue) => setVolumeInput(newValue as number)}
onChangeCommitted={(e, newValue) => {
const newVolume = newValue as number;
console.log('Volume change:', newVolume);
if (onSave) onSave({ ...metadata, volume: newVolume });
}}
color="secondary"
className="p-0 m-0"
/>
{/* <input
type="range"
min={0}
max={1}
step={0.01}
value={volumeInput}
onChange={(e) => {
const newVolume = parseFloat(e.target.value);
setVolumeInput(newVolume);
}}
onDragEnd={(e) => {
console.log('Volume change:');
// const newVolume = parseFloat(e.target.value);
// if (onSave) onSave({ ...metadata, volume: newVolume });
}}
className="mx-2 w-full accent-plum"
aria-label="Volume slider"
/> */}
</div>
<div className="w-1/5 flex justify-end text-sm text-neutral-500">
<ToggleButtonGroup value={metadata.playbackType}>
<ToggleButton
value="playStop"
color="primary"
onClick={() => {
if (onSave)
onSave({
...metadata,
playbackType: PlaybackType.PlayStop,
});
}}
>
<PlayStopIcon />
</ToggleButton>
<ToggleButton
value="playOverlap"
color="primary"
onClick={() => {
if (onSave)
onSave({
...metadata,
playbackType: PlaybackType.PlayOverlap,
});
}}
>
<PlayOverlapIcon />
</ToggleButton>
</ToggleButtonGroup>
</div> </div>
</div> </div>
</div> </div>

View File

@ -15,6 +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';
export interface ClipListProps { export interface ClipListProps {
collection: string; collection: string;
@ -31,6 +32,33 @@ export default function ClipList({ collection }: ClipListProps) {
useSensor(PointerSensor, { activationConstraint: { distance: 5 } }), useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
); );
const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
console.log('Files dropped:', event.dataTransfer.files);
const files = Array.from(event.dataTransfer.files).filter((file) =>
file.type.startsWith('audio/'),
);
if (files.length > 0) {
const formData = new FormData();
files.forEach((file) => formData.append('files', file));
// todo send the file to the backend and add to the collection
// fetch('http://localhost:5010/file/upload', {
// method: 'POST',
// body: formData,
// })
// .then((res) => res.json())
// .catch((err) => console.error('Error uploading files:', err));
// Implement your onDrop logic here
// onDrop(files, selectedCollection);
}
};
const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
};
async function handleDragEnd(event: any) { async function handleDragEnd(event: any) {
const { active, over } = event; const { active, over } = event;
if (active.id !== over?.id) { if (active.id !== over?.id) {
@ -50,19 +78,16 @@ export default function ClipList({ collection }: ClipListProps) {
payload: { collection, newMetadata }, payload: { collection, newMetadata },
}); });
try { try {
const response = await fetch( const response = await apiFetch('meta/collection/clips/reorder', {
'http://localhost:5010/meta/collection/clips/reorder', method: 'POST',
{ headers: {
method: 'POST', 'Content-Type': 'application/json',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: collection,
clips: newMetadata.clips,
}),
}, },
); body: JSON.stringify({
name: collection,
clips: newMetadata.clips,
}),
});
const data = await response.json(); const data = await response.json();
console.log('handle reorder return:', data.collections); console.log('handle reorder return:', data.collections);
dispatch({ type: 'metadata/setAllData', payload: data }); dispatch({ type: 'metadata/setAllData', payload: data });
@ -78,7 +103,7 @@ export default function ClipList({ collection }: ClipListProps) {
type: 'metadata/deleteClip', type: 'metadata/deleteClip',
payload: { collection, clip: meta }, payload: { collection, clip: meta },
}); });
fetch('http://localhost:5010/meta/collection/clips/remove', { apiFetch('meta/collection/clips/remove', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -99,7 +124,7 @@ export default function ClipList({ collection }: ClipListProps) {
type: 'metadata/moveClip', type: 'metadata/moveClip',
payload: { sourceCollection: collection, targetCollection, clip: meta }, payload: { sourceCollection: collection, targetCollection, clip: meta },
}); });
fetch('http://localhost:5010/meta/collection/clips/move', { apiFetch('meta/collection/clips/move', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -120,19 +145,16 @@ export default function ClipList({ collection }: ClipListProps) {
type: 'metadata/editClip', type: 'metadata/editClip',
payload: { collection, clip: meta }, payload: { collection, clip: meta },
}); });
const response = await fetch( const response = await apiFetch('meta/collection/clips/edit', {
'http://localhost:5010/meta/collection/clips/edit', method: 'POST',
{ headers: {
method: 'POST', 'Content-Type': 'application/json',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: collection,
clip: meta,
}),
}, },
); body: JSON.stringify({
name: collection,
clip: meta,
}),
});
await response.json(); await response.json();
// console.log('handle clip save return:', data.collections); // console.log('handle clip save return:', data.collections);
dispatch({ dispatch({
@ -145,7 +167,11 @@ export default function ClipList({ collection }: ClipListProps) {
} }
return ( return (
<div className="min-h-full flex flex-col justify-start bg-midnight text-offwhite"> <div
className="min-h-full flex flex-col justify-start bg-midnight text-offwhite"
onDrop={handleDrop}
onDragOver={handleDragOver}
>
<DndContext <DndContext
sensors={sensors} sensors={sensors}
collisionDetection={closestCenter} collisionDetection={closestCenter}

View File

@ -0,0 +1,29 @@
import React from 'react';
export default function PlayOverlapIcon({
size = 24,
color = 'currentColor',
}: {
size?: number;
color?: string;
}) {
return (
<svg
width={size}
height={size}
viewBox="0 0 32 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{/* Filled play arrow */}
<polygon points="4,4 4,20 16,12" fill={color} />
{/* Outlined play arrow (underneath and to the right) */}
<polygon
points="12,4 12,20 24,12"
fill="none"
stroke={color}
strokeWidth={1}
/>
</svg>
);
}

View File

@ -0,0 +1,23 @@
export default function PlayStopIcon({
size = 24,
color = 'currentColor',
}: {
size?: number;
color?: string;
}) {
return (
<svg
width={size}
height={size}
viewBox="0 0 48 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-label="Play/Stop Icon"
>
{/* Play Arrow */}
<polygon points="4,4 20,12 4,20" fill={color} />
{/* Stop Square */}
<rect x="28" y="4" width="16" height="16" rx="2" fill={color} />
</svg>
);
}

View File

@ -1,10 +1,10 @@
import { ElectronHandler, FileHandler } from '../main/preload'; import { ElectronHandler, AudioHandler } from '../main/preload';
declare global { declare global {
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
interface Window { interface Window {
electron: ElectronHandler; electron: ElectronHandler;
audio: FileHandler; audio: AudioHandler;
} }
} }

View File

@ -4,4 +4,5 @@ packages/
ClipTrimDotNet/bin/ ClipTrimDotNet/bin/
ClipTrimDotNet/obj/ ClipTrimDotNet/obj/
ClipTrimDotNet/dist/ ClipTrimDotNet/dist/
ClipTrimDotNet/node_modules/ ClipTrimDotNet/node_modules/
.vs/

View File

@ -1,96 +0,0 @@
{
"Version": 1,
"WorkspaceRootPath": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\",
"Documents": [
{
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\client\\cliptrimclient.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\client\\cliptrimclient.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
},
{
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\player.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\player.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
},
{
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\profileswitcher.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\profileswitcher.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
},
{
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\client\\collectionmetadata.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\client\\collectionmetadata.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
}
],
"DocumentGroupContainers": [
{
"Orientation": 0,
"VerticalTabListWidth": 256,
"DocumentGroups": [
{
"DockedWidth": 297,
"SelectedChildIndex": 2,
"Children": [
{
"$type": "Bookmark",
"Name": "ST:0:0:{57d563b6-44a5-47df-85be-f4199ad6b651}"
},
{
"$type": "Document",
"DocumentIndex": 2,
"Title": "ProfileSwitcher.cs",
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\ProfileSwitcher.cs",
"RelativeDocumentMoniker": "ClipTrimDotNet\\ProfileSwitcher.cs",
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\ProfileSwitcher.cs",
"RelativeToolTip": "ClipTrimDotNet\\ProfileSwitcher.cs",
"ViewState": "AgIAAFkAAAAAAAAAAAAlwG8AAABKAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-02-21T15:06:24.045Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 0,
"Title": "ClipTrimClient.cs",
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Client\\ClipTrimClient.cs",
"RelativeDocumentMoniker": "ClipTrimDotNet\\Client\\ClipTrimClient.cs",
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Client\\ClipTrimClient.cs",
"RelativeToolTip": "ClipTrimDotNet\\Client\\ClipTrimClient.cs",
"ViewState": "AgIAAEgAAAAAAAAAAAAuwGMAAAAJAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-02-21T15:03:49.814Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 3,
"Title": "CollectionMetaData.cs",
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Client\\CollectionMetaData.cs",
"RelativeDocumentMoniker": "ClipTrimDotNet\\Client\\CollectionMetaData.cs",
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Client\\CollectionMetaData.cs",
"RelativeToolTip": "ClipTrimDotNet\\Client\\CollectionMetaData.cs",
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-02-21T15:03:47.862Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 1,
"Title": "Player.cs",
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Player.cs",
"RelativeDocumentMoniker": "ClipTrimDotNet\\Player.cs",
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Player.cs*",
"RelativeToolTip": "ClipTrimDotNet\\Player.cs*",
"ViewState": "AgIAAHIAAAAAAAAAAAA3wIYAAABMAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-02-21T15:00:23.762Z",
"EditorCaption": ""
},
{
"$type": "Bookmark",
"Name": "ST:0:0:{cce594b6-0c39-4442-ba28-10c64ac7e89f}"
}
]
}
]
}
]
}

View File

@ -1,113 +0,0 @@
{
"Version": 1,
"WorkspaceRootPath": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\",
"Documents": [
{
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\wavplayer.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\wavplayer.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
},
{
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\player.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\player.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
},
{
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\profileswitcher.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\profileswitcher.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
},
{
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\client\\cliptrimclient.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\client\\cliptrimclient.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
},
{
"AbsoluteMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|c:\\users\\mickl\\desktop\\cliptrim-ui\\cliptrimapp\\stream_deck_plugin\\cliptrimdotnet\\client\\collectionmetadata.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}",
"RelativeMoniker": "D:0:0:{4635D874-69C0-4010-BE46-77EF92EB1553}|ClipTrimDotNet\\ClipTrimDotNet.csproj|solutionrelative:cliptrimdotnet\\client\\collectionmetadata.cs||{A6C744A8-0E4A-4FC6-886A-064283054674}"
}
],
"DocumentGroupContainers": [
{
"Orientation": 0,
"VerticalTabListWidth": 256,
"DocumentGroups": [
{
"DockedWidth": 297,
"SelectedChildIndex": 1,
"Children": [
{
"$type": "Bookmark",
"Name": "ST:0:0:{57d563b6-44a5-47df-85be-f4199ad6b651}"
},
{
"$type": "Document",
"DocumentIndex": 0,
"Title": "WavPlayer.cs",
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\WavPlayer.cs",
"RelativeDocumentMoniker": "ClipTrimDotNet\\WavPlayer.cs",
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\WavPlayer.cs",
"RelativeToolTip": "ClipTrimDotNet\\WavPlayer.cs",
"ViewState": "AgIAALYAAAAAAAAAAAAAALsAAAANAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-02-21T15:16:26.477Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 2,
"Title": "ProfileSwitcher.cs",
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\ProfileSwitcher.cs",
"RelativeDocumentMoniker": "ClipTrimDotNet\\ProfileSwitcher.cs",
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\ProfileSwitcher.cs",
"RelativeToolTip": "ClipTrimDotNet\\ProfileSwitcher.cs",
"ViewState": "AgIAAG8AAAAAAAAAAAAWwG8AAABKAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-02-21T15:06:24.045Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 3,
"Title": "ClipTrimClient.cs",
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Client\\ClipTrimClient.cs",
"RelativeDocumentMoniker": "ClipTrimDotNet\\Client\\ClipTrimClient.cs",
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Client\\ClipTrimClient.cs",
"RelativeToolTip": "ClipTrimDotNet\\Client\\ClipTrimClient.cs",
"ViewState": "AgIAAEgAAAAAAAAAAAAuwGIAAAApAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-02-21T15:03:49.814Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 4,
"Title": "CollectionMetaData.cs",
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Client\\CollectionMetaData.cs",
"RelativeDocumentMoniker": "ClipTrimDotNet\\Client\\CollectionMetaData.cs",
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Client\\CollectionMetaData.cs",
"RelativeToolTip": "ClipTrimDotNet\\Client\\CollectionMetaData.cs",
"ViewState": "AgIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-02-21T15:03:47.862Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 1,
"Title": "Player.cs",
"DocumentMoniker": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Player.cs",
"RelativeDocumentMoniker": "ClipTrimDotNet\\Player.cs",
"ToolTip": "C:\\Users\\mickl\\Desktop\\cliptrim-ui\\ClipTrimApp\\stream_deck_plugin\\ClipTrimDotNet\\Player.cs",
"RelativeToolTip": "ClipTrimDotNet\\Player.cs",
"ViewState": "AgIAAHoAAAAAAAAAAAAswIwAAAAbAAAAAAAAAA==",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.000738|",
"WhenOpened": "2026-02-21T15:00:23.762Z",
"EditorCaption": ""
},
{
"$type": "Bookmark",
"Name": "ST:0:0:{cce594b6-0c39-4442-ba28-10c64ac7e89f}"
}
]
}
]
}
]
}

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,158 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json; using Newtonsoft.Json;
using SocketIOClient;
namespace ClipTrimDotNet.Client using BarRaider.SdTools;
{ using System.Runtime.CompilerServices;
public class ClipTrimClient
{ namespace ClipTrimDotNet.Client
private static ClipTrimClient? instance; {
public static ClipTrimClient Instance public class ClipTrimClient
{ {
get private static ClipTrimClient? instance;
{ public static ClipTrimClient Instance
if (instance == null) {
{ get
instance = new ClipTrimClient(); {
} if (instance == null)
return instance; {
} instance = new ClipTrimClient();
} }
return instance;
private HttpClient httpClient; }
}
public ClipTrimClient()
{ //private HttpClient httpClient;
httpClient = new HttpClient() private SocketIO socket;
{
BaseAddress = new Uri("http://localhost:5010/"), public int PortNumber { get; set; } = 5010;
Timeout = TimeSpan.FromSeconds(10)
}; public ClipTrimClient()
Task.Run(ShortPoll); {
} //httpClient = new HttpClient()
//{
public async Task ShortPoll() // BaseAddress = new Uri("http://localhost:5010/"),
{ // Timeout = TimeSpan.FromSeconds(10)
while (true) //};
{ Logger.Instance.LogMessage(TracingLevel.INFO, $"Starting ClipTrimClient on port {PortNumber}");
await GetMetadata(); socket = new SocketIO(new Uri($"http://localhost:5010/"));
await Task.Delay(TimeSpan.FromSeconds(5)); await Task.Delay(TimeSpan.FromSeconds(5)); socket.Options.AutoUpgrade = false;
socket.Options.ConnectionTimeout = TimeSpan.FromSeconds(10);
} socket.Options.Reconnection = true;
} socket.On("full_data", ctx =>
{
public List<CollectionMetaData> Collections { get; private set; } = new List<CollectionMetaData>(); try
public CollectionMetaData? SelectedCollection { get; private set; } {
public int PageIndex { get; private set; } = 0; var response = ctx.GetValue<List<CollectionMetaData>>(0);
private async Task GetMetadata() Logger.Instance.LogMessage(TracingLevel.INFO, $"full_data event {JsonConvert.SerializeObject(response)}");
{ Collections = response!;
try //Logger.Instance.LogMessage(TracingLevel.INFO, $"Collections {JsonConvert.SerializeObject(Collections)}");
{ }
var response = await httpClient.GetAsync("meta"); catch (Exception ex)
if (response.IsSuccessStatusCode) {
{ Logger.Instance.LogMessage(TracingLevel.INFO, $"full_data error {ex.ToString()}");
var json = await response.Content.ReadAsStringAsync(); }
dynamic collections = JsonConvert.DeserializeObject(json); return Task.CompletedTask;
collections = collections.collections; });
Collections = JsonConvert.DeserializeObject<List<CollectionMetaData>>(collections.ToString()); socket.On("collection_updated", ctx =>
} {
} try
catch (Exception ex) {
{ var response = ctx.GetValue<CollectionMetaData>(0)!;
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Error pinging ClipTrim API: {ex.Message}"); Logger.Instance.LogMessage(TracingLevel.INFO, $"collection_updated event {JsonConvert.SerializeObject(response)}");
return; int index = Collections.FindIndex(x => x.Id == response.Id);
} if (index != -1)
{
} Collections[index] = response;
}
public List<string> GetCollectionNames() }
{ catch
//await GetMetadata(); {
return Collections.Select(x => x.Name).ToList();
} }
public void SetSelectedCollectionByName(string name) return Task.CompletedTask;
{ });
var collection = Collections.FirstOrDefault(x => x.Name == name);
if (collection != null) Task.Run(async () => await socket.ConnectAsync());
{ //Task.Run(ShortPoll);
SelectedCollection = collection; }
PageIndex = 0;
}
} //public async Task ShortPoll()
//{
public ClipMetadata? GetClipByPagedIndex(int index) // while (true)
{ // {
if (SelectedCollection == null) return null; // await GetMetadata();
int clipIndex = PageIndex * 10 + index; // await Task.Delay(TimeSpan.FromSeconds(5)); await Task.Delay(TimeSpan.FromSeconds(5));
if (clipIndex >= 0 && clipIndex < SelectedCollection.Clips.Count)
{ // }
return SelectedCollection.Clips[clipIndex]; //}
}
return null; public List<CollectionMetaData> Collections { get; private set; } = new List<CollectionMetaData>();
} public int SelectedCollection { get; private set; } = -1;
public int PageIndex { get; private set; } = 0;
public async void PlayClip(ClipMetadata? metadata) //private async Task GetMetadata()
{ //{
if (metadata == null) return; // try
// {
var response = await httpClient.PostAsync("playback/start", new StringContent(JsonConvert.SerializeObject(metadata), Encoding.UTF8, "application/json")); // var response = await httpClient.GetAsync("meta");
if (!response.IsSuccessStatusCode) // if (response.IsSuccessStatusCode)
{ // {
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Error playing clip: {response.ReasonPhrase}"); // var json = await response.Content.ReadAsStringAsync();
} // dynamic collections = JsonConvert.DeserializeObject(json);
} // collections = collections.collections;
} // Collections = JsonConvert.DeserializeObject<List<CollectionMetaData>>(collections.ToString());
} // }
// }
// catch (Exception ex)
// {
// //Logger.Instance.LogMessage(TracingLevel.INFO, $"Error pinging ClipTrim API: {ex.Message}");
// return;
// }
//}
public List<string> GetCollectionNames()
{
//await GetMetadata();
return Collections.Select(x => x.Name).ToList();
}
public void SetSelectedCollectionByName(string name)
{
SelectedCollection = Collections.FindIndex(x => x.Name == name);
if (SelectedCollection != -1)
{
PageIndex = 0;
}
}
public ClipMetadata? GetClipByPagedIndex(int index)
{
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(ClipMetadata? metadata)
{
if (metadata == null) return;
//var response = await httpClient.PostAsync("playback/start", new StringContent(JsonConvert.SerializeObject(metadata), Encoding.UTF8, "application/json"));
//if (!response.IsSuccessStatusCode)
//{
// //Logger.Instance.LogMessage(TracingLevel.INFO, $"Error playing clip: {response.ReasonPhrase}");
//}
}
}
}

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,95 @@
<?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> <PackageReference Include="CommandLineParser" Version="2.9.1" />
<DefineConstants>DEBUG;TRACE</DefineConstants> <PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="8.0.0" />
<ErrorReport>prompt</ErrorReport> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
<WarningLevel>4</WarningLevel> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.2" />
</PropertyGroup> <PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.2" />
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' "> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.2" />
<PlatformTarget>AnyCPU</PlatformTarget> <PackageReference Include="Microsoft.Extensions.Options" Version="10.0.2" />
<DebugType>pdbonly</DebugType> <PackageReference Include="Microsoft.Extensions.Primitives" Version="10.0.2" />
<Optimize>true</Optimize> <PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<OutputPath>bin\Release\ClipTrimDotNet.sdPlugin\</OutputPath> <PackageReference Include="NLog" Version="6.0.5" />
<DefineConstants>TRACE</DefineConstants> <PackageReference Include="SocketIOClient" Version="4.0.0.2" />
<ErrorReport>prompt</ErrorReport> <PackageReference Include="SocketIOClient.Common" Version="4.0.0" />
<WarningLevel>4</WarningLevel> <PackageReference Include="SocketIOClient.Serializer" Version="4.0.0.1" />
</PropertyGroup> <PackageReference Include="SocketIOClient.Serializer.NewtonsoftJson" Version="4.0.0.1" />
<ItemGroup> <PackageReference Include="StreamDeck-Tools" Version="6.3.2" />
<Reference Include="CommandLine, Version=2.9.1.0, Culture=neutral, PublicKeyToken=5a870481e358d379, processorArchitecture=MSIL"> <PackageReference Include="System.Diagnostics.DiagnosticSource" Version="10.0.2" />
<HintPath>..\packages\CommandLineParser.2.9.1\lib\net461\CommandLine.dll</HintPath> <PackageReference Include="System.Drawing.Common" Version="9.0.10" />
</Reference> <PackageReference Include="System.IO.Pipelines" Version="10.0.2" />
<Reference Include="Microsoft.Win32.Registry, Version=4.1.3.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> <PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="6.1.2" />
<HintPath>..\packages\Microsoft.Win32.Registry.4.7.0\lib\net461\Microsoft.Win32.Registry.dll</HintPath> <PackageReference Include="System.Security.AccessControl" Version="6.0.1" />
</Reference> <PackageReference Include="System.Text.Encodings.Web" Version="10.0.2" />
<Reference Include="NAudio, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL"> <PackageReference Include="System.Text.Json" Version="10.0.2" />
<HintPath>..\packages\NAudio.2.2.1\lib\net472\NAudio.dll</HintPath> <PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.3" />
</Reference> <PackageReference Include="CoreWCF.Primitives" Version="1.8.0" />
<Reference Include="NAudio.Asio, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL"> <PackageReference Include="CoreWCF.ConfigurationManager" Version="1.8.0" />
<HintPath>..\packages\NAudio.Asio.2.2.1\lib\netstandard2.0\NAudio.Asio.dll</HintPath> <PackageReference Include="CoreWCF.Http" Version="1.8.0" />
</Reference> <PackageReference Include="CoreWCF.WebHttp" Version="1.8.0" />
<Reference Include="NAudio.Core, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL"> <PackageReference Include="CoreWCF.NetTcp" Version="1.8.0" />
<HintPath>..\packages\NAudio.Core.2.2.1\lib\netstandard2.0\NAudio.Core.dll</HintPath> </ItemGroup>
</Reference> <ItemGroup>
<Reference Include="NAudio.Midi, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL"> <None Update="DialLayout.json">
<HintPath>..\packages\NAudio.Midi.2.2.1\lib\netstandard2.0\NAudio.Midi.dll</HintPath> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Reference> </None>
<Reference Include="NAudio.Wasapi, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL"> <None Update="manifest.json">
<HintPath>..\packages\NAudio.Wasapi.2.2.1\lib\netstandard2.0\NAudio.Wasapi.dll</HintPath> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Reference> </None>
<Reference Include="NAudio.WinForms, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL"> </ItemGroup>
<HintPath>..\packages\NAudio.WinForms.2.2.1\lib\net472\NAudio.WinForms.dll</HintPath> <ItemGroup>
</Reference> <Content Include="!!README!!.txt" />
<Reference Include="NAudio.WinMM, Version=2.2.1.0, Culture=neutral, PublicKeyToken=e279aa5131008a41, processorArchitecture=MSIL"> <Content Include="Images\categoryIcon%402x.png">
<HintPath>..\packages\NAudio.WinMM.2.2.1\lib\netstandard2.0\NAudio.WinMM.dll</HintPath> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Reference> </Content>
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL"> <Content Include="Images\categoryIcon.png">
<HintPath>..\packages\Newtonsoft.Json.13.0.4\lib\net45\Newtonsoft.Json.dll</HintPath> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Reference> </Content>
<Reference Include="NLog, Version=6.0.0.0, Culture=neutral, PublicKeyToken=5120e14c03d0593c, processorArchitecture=MSIL"> <Content Include="Images\icon%402x.png">
<HintPath>..\packages\NLog.6.0.5\lib\net46\NLog.dll</HintPath> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Reference> </Content>
<Reference Include="StreamDeckTools, Version=6.3.2.0, Culture=neutral, processorArchitecture=MSIL"> <Content Include="Images\icon.png">
<HintPath>..\packages\StreamDeck-Tools.6.3.2\lib\netstandard2.0\StreamDeckTools.dll</HintPath> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Reference> </Content>
<Reference Include="System" /> <Content Include="Images\pluginAction%402x.png">
<Reference Include="System.Configuration" /> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Reference Include="System.Core" /> </Content>
<Reference Include="System.Drawing" /> <Content Include="Images\pluginAction.png">
<Reference Include="System.Drawing.Common, Version=9.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<HintPath>..\packages\System.Drawing.Common.9.0.10\lib\net462\System.Drawing.Common.dll</HintPath> </Content>
</Reference> <Content Include="Images\pluginIcon%402x.png">
<Reference Include="System.IO.Compression" /> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Reference Include="System.Runtime.Serialization" /> </Content>
<Reference Include="System.Security.AccessControl, Version=4.1.3.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> <Content Include="Images\pluginIcon.png">
<HintPath>..\packages\System.Security.AccessControl.4.7.0\lib\net461\System.Security.AccessControl.dll</HintPath> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Reference> </Content>
<Reference Include="System.Security.Principal.Windows, Version=4.1.3.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL"> <Content Include="package.json" />
<HintPath>..\packages\System.Security.Principal.Windows.4.7.0\lib\net461\System.Security.Principal.Windows.dll</HintPath> <Content Include="PropertyInspector\profile_swticher.html">
</Reference> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Reference Include="System.ServiceModel" /> </Content>
<Reference Include="System.Transactions" /> <Content Include="PropertyInspector\file_player.html">
<Reference Include="System.Xml.Linq" /> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Reference Include="System.Data.DataSetExtensions" /> </Content>
<Reference Include="Microsoft.CSharp" /> </ItemGroup>
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="BaseTest.cs" />
<Compile Include="Client\ClipMetadata.cs" />
<Compile Include="Client\ClipTrimClient.cs" />
<Compile Include="Client\CollectionMetaData.cs" />
<Compile Include="GlobalSettings.cs" />
<Compile Include="Player.cs" />
<Compile Include="ProfileSwitcher.cs" />
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="WavPlayer.cs" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<None Include="DialLayout.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="manifest.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Content Include="!!README!!.txt" />
<Content Include="Images\categoryIcon%402x.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Images\categoryIcon.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Images\icon%402x.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Images\icon.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Images\pluginAction%402x.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Images\pluginAction.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Images\pluginIcon%402x.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Images\pluginIcon.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="package.json" />
<Content Include="PropertyInspector\profile_swticher.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="PropertyInspector\file_player.html">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PreBuildEvent>npm run stop</PreBuildEvent>
</PropertyGroup>
<PropertyGroup>
<PostBuildEvent>npm run start</PostBuildEvent>
</PropertyGroup>
</Project> </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,108 @@
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 FileEntry
public class FileEntry {
{ public FileEntry()
public FileEntry() {
{ Volume = 1.0;
Volume = 1.0; Playtype = "Play/Overlap";
Playtype = "Play/Overlap"; }
} [JsonProperty(PropertyName = "Volume")]
[JsonProperty(PropertyName = "Volume")] public double Volume { get; set; }
public double Volume { get; set; } [JsonProperty(PropertyName = "Playtype")]
[JsonProperty(PropertyName = "Playtype")] public string Playtype { get; set; }
public string Playtype { get; set; } }
} public class CollectionEntry
public class CollectionEntry {
{ public CollectionEntry()
public CollectionEntry() {
{ Files = new Dictionary<string, FileEntry>();
Files = new Dictionary<string, FileEntry>(); }
} [JsonProperty(PropertyName = "Files")]
[JsonProperty(PropertyName = "Files")] public Dictionary<string, FileEntry> Files { get; set; }
public Dictionary<string, FileEntry> Files { get; set; } }
}
public class GlobalSettings
public class GlobalSettings {
{ public static GlobalSettings? _inst;
public static GlobalSettings? _inst; public static GlobalSettings Instance
public static GlobalSettings Instance {
{ get
get {
{ _inst ??= CreateDefaultSettings();
_inst ??= CreateDefaultSettings(); return _inst;
return _inst; }
} set
set {
{ _inst = value;
_inst = value; }
} }
} public static GlobalSettings CreateDefaultSettings()
public static GlobalSettings CreateDefaultSettings() {
{ GlobalSettings instance = new GlobalSettings();
GlobalSettings instance = new GlobalSettings(); instance.BasePath = null;
instance.BasePath = null; instance.ProfileName = null;
instance.ProfileName = null; instance.Collections = new Dictionary<string, CollectionEntry>();
instance.Collections = new Dictionary<string, CollectionEntry>(); return instance;
return instance; }
}
[FilenameProperty]
[FilenameProperty] [JsonProperty(PropertyName = "basePath")]
[JsonProperty(PropertyName = "basePath")] public string? BasePath { get; set; }
public string? BasePath { get; set; }
[JsonProperty(PropertyName = "profileName")]
[JsonProperty(PropertyName = "profileName")] public string? ProfileName { get; set; }
public string? ProfileName { get; set; }
[JsonProperty(PropertyName = "outputDevice")]
[JsonProperty(PropertyName = "outputDevice")] public string? OutputDevice { get; set; }
public string? OutputDevice { get; set; }
[JsonProperty(PropertyName = "collections")]
[JsonProperty(PropertyName = "collections")] public Dictionary<string, CollectionEntry> Collections { get; set; }
public Dictionary<string, CollectionEntry> Collections { get; set; }
public void SetCurrentProfile(string profile)
public void SetCurrentProfile(string profile) {
{ ProfileName = profile;
ProfileName = profile; if(!Collections.ContainsKey(profile))
if(!Collections.ContainsKey(profile)) {
{ Collections.Add(profile, new CollectionEntry());
Collections.Add(profile, new CollectionEntry()); }
} }
}
public FileEntry GetFileOptionsInCurrentProfile(string filename)
public FileEntry GetFileOptionsInCurrentProfile(string filename) {
{ if(!Collections.TryGetValue(ProfileName, out CollectionEntry collection))
if(!Collections.TryGetValue(ProfileName, out CollectionEntry collection)) {
{ return new FileEntry();
return new FileEntry(); }
} if(!collection.Files.TryGetValue(filename, out FileEntry file))
if(!collection.Files.TryGetValue(filename, out FileEntry file)) {
{ return new FileEntry();
return new FileEntry(); }
} //Logger.Instance.LogMessage(TracingLevel.INFO, "fetched file settings " + filename + JsonConvert.SerializeObject(file));
Logger.Instance.LogMessage(TracingLevel.INFO, "fetched file settings " + filename + JsonConvert.SerializeObject(file)); return file;
return file; }
}
public void SetFileOptionsCurrentProfile(string filename, FileEntry file)
public void SetFileOptionsCurrentProfile(string filename, FileEntry file) {
{ //Logger.Instance.LogMessage(TracingLevel.INFO, "SetFileOptionsCurrentProfile ");
Logger.Instance.LogMessage(TracingLevel.INFO, "SetFileOptionsCurrentProfile "); if (!Collections.TryGetValue(ProfileName, out CollectionEntry collection))
if (!Collections.TryGetValue(ProfileName, out CollectionEntry collection)) {
{ return;
return; }
} //Logger.Instance.LogMessage(TracingLevel.INFO, "SetFileOptionsCurrentProfile 2");
Logger.Instance.LogMessage(TracingLevel.INFO, "SetFileOptionsCurrentProfile 2"); //collection.Files[filename] = file;
//collection.Files[filename] = file; Collections[ProfileName].Files[filename] = file;
Collections[ProfileName].Files[filename] = file; }
} }
} }
}

View File

@ -1,193 +1,194 @@
using BarRaider.SdTools; using BarRaider.SdTools;
using BarRaider.SdTools.Wrappers; using BarRaider.SdTools.Wrappers;
using ClipTrimDotNet.Client; using ClipTrimDotNet.Client;
using NAudio.CoreAudioApi.Interfaces; using System.Text.Json.Serialization;
using Newtonsoft.Json; using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Linq; using System;
using System; using System.Collections.Generic;
using System.Collections.Generic; using System.Drawing;
using System.Drawing; using System.Dynamic;
using System.Dynamic; using System.IO;
using System.IO; using System.Linq;
using System.Linq; using System.Text;
using System.Text; using System.Threading.Tasks;
using System.Threading.Tasks;
namespace ClipTrimDotNet
namespace ClipTrimDotNet {
{ [PluginActionId("com.michal-courson.cliptrim.player")]
[PluginActionId("com.michal-courson.cliptrim.player")] public class Player : KeypadBase
public class Player : KeypadBase {
{
private ClipMetadata? metadata;
private ClipMetadata? metadata; private KeyCoordinates coordinates;
private KeyCoordinates coordinates; private class PluginSettings
private class PluginSettings {
{ public static PluginSettings CreateDefaultSettings()
public static PluginSettings CreateDefaultSettings() {
{ PluginSettings instance = new PluginSettings();
PluginSettings instance = new PluginSettings(); instance.Path = null;
instance.Path = null; instance.PlayType = "Play/Overlap";
instance.PlayType = "Play/Overlap"; instance.Index = 0;
instance.Index = 0; instance.Volume = 1;
instance.Volume = 1; return instance;
return instance; }
}
[FilenameProperty]
[FilenameProperty] [JsonPropertyName("path")]
[JsonProperty(PropertyName = "path")] public string? Path { get; set; }
public string? Path { get; set; }
[JsonPropertyName("index")]
[JsonProperty(PropertyName = "index")] public int? Index { get; set; }
public int? Index { get; set; } [JsonPropertyName("playtype")]
[JsonProperty(PropertyName = "playtype")] public string PlayType { get; set; }
public string PlayType { get; set; }
[JsonPropertyName("volume")]
[JsonProperty(PropertyName = "volume")] public double Volume { get; set; }
public double Volume { get; set; } }
}
#region Private Members
#region Private Members
private PluginSettings settings;
private PluginSettings settings;
#endregion
#endregion public Player(SDConnection connection, InitialPayload payload) : base(connection, payload)
public Player(SDConnection connection, InitialPayload payload) : base(connection, payload) {
{ if (payload.Settings == null || payload.Settings.Count == 0)
if (payload.Settings == null || payload.Settings.Count == 0) {
{ this.settings = PluginSettings.CreateDefaultSettings();
this.settings = PluginSettings.CreateDefaultSettings(); SaveSettings();
SaveSettings(); }
} else
else {
{ this.settings = payload.Settings.ToObject<PluginSettings>();
this.settings = payload.Settings.ToObject<PluginSettings>(); }
} this.coordinates = payload.Coordinates;
this.coordinates = payload.Coordinates; GlobalSettingsManager.Instance.RequestGlobalSettings();
GlobalSettingsManager.Instance.RequestGlobalSettings(); CheckFile();
CheckFile(); }
}
private void Instance_OnReceivedGlobalSettings(object sender, ReceivedGlobalSettingsPayload e)
private void Instance_OnReceivedGlobalSettings(object sender, ReceivedGlobalSettingsPayload e) {
{ Tools.AutoPopulateSettings(GlobalSettings.Instance, e.Settings);
Tools.AutoPopulateSettings(GlobalSettings.Instance, e.Settings); }
}
public int GetIndex()
public int GetIndex() {
{ return Math.Max((coordinates.Row - 1) * 5 + coordinates.Column, 0);
return Math.Max((coordinates.Row - 1) * 5 + coordinates.Column, 0); }
}
private async void CheckFile()
private async void CheckFile() {
{
//if (settings == null || GlobalSettings.Instance.ProfileName ==null) return;
//if (settings == null || GlobalSettings.Instance.ProfileName ==null) return; metadata = ClipTrimClient.Instance.GetClipByPagedIndex(GetIndex());
metadata = ClipTrimClient.Instance.GetClipByPagedIndex(GetIndex()); await Connection.SetTitleAsync($"{metadata?.Name ?? ""}");
await Connection.SetTitleAsync($"{metadata?.Name ?? ""}"); //Logger.Instance.LogMessage(TracingLevel.INFO, $"Set title to {metadata?.Name ?? ""}");
return; return;
//var files = Directory.GetFiles(Path.Combine(Path.GetDirectoryName(GlobalSettings.Instance.BasePath), GlobalSettings.Instance.ProfileName), "*.wav", SearchOption.TopDirectoryOnly) //var files = Directory.GetFiles(Path.Combine(Path.GetDirectoryName(GlobalSettings.Instance.BasePath), GlobalSettings.Instance.ProfileName), "*.wav", SearchOption.TopDirectoryOnly)
// .OrderBy(file => File.GetCreationTime(file)) // .OrderBy(file => File.GetCreationTime(file))
// .ToArray(); // .ToArray();
//int? i = this.settings.Index; //int? i = this.settings.Index;
//string new_path = ""; //string new_path = "";
//if (i != null && i >= 0 && i < files.Length) //if (i != null && i >= 0 && i < files.Length)
//{ //{
// new_path = files[i ?? 0]; // new_path = files[i ?? 0];
//} //}
//await Connection.SetTitleAsync(Path.GetFileNameWithoutExtension(new_path)); //await Connection.SetTitleAsync(Path.GetFileNameWithoutExtension(new_path));
//if (new_path != settings.Path) //if (new_path != settings.Path)
//{ //{
// settings.Path = new_path; // settings.Path = new_path;
// if(new_path != "") // if(new_path != "")
// { // {
// FileEntry opts = GlobalSettings.Instance.GetFileOptionsInCurrentProfile(new_path); // FileEntry opts = GlobalSettings.Instance.GetFileOptionsInCurrentProfile(new_path);
// settings.Volume = opts.Volume; // settings.Volume = opts.Volume;
// settings.PlayType = opts.Playtype; // settings.PlayType = opts.Playtype;
// } // }
// await SaveSettings(); // await SaveSettings();
//} //}
} }
private async void Connection_OnApplicationDidLaunch(object sender, BarRaider.SdTools.Wrappers.SDEventReceivedEventArgs<BarRaider.SdTools.Events.ApplicationDidLaunch> e) 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) private void Connection_OnTitleParametersDidChange(object sender, SDEventReceivedEventArgs<BarRaider.SdTools.Events.TitleParametersDidChange> e)
{ {
//titleParameters = e.Event?.Payload?.TitleParameters; //titleParameters = e.Event?.Payload?.TitleParameters;
//userTitle = e.Event?.Payload?.Title; //userTitle = e.Event?.Payload?.Title;
} }
public override void Dispose() public override void Dispose()
{ {
Connection.OnTitleParametersDidChange -= Connection_OnTitleParametersDidChange; Connection.OnTitleParametersDidChange -= Connection_OnTitleParametersDidChange;
Connection.OnApplicationDidLaunch -= Connection_OnApplicationDidLaunch; Connection.OnApplicationDidLaunch -= Connection_OnApplicationDidLaunch;
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Destructor called"); //Logger.Instance.LogMessage(TracingLevel.INFO, $"Destructor called");
} }
public override void KeyPressed(KeyPayload payload) public override void KeyPressed(KeyPayload payload)
{ {
//Logger.Instance.LogMessage(TracingLevel.INFO, "Key Pressedd"); //Logger.Instance.LogMessage(TracingLevel.INFO, "Key Pressedd");
Tools.AutoPopulateSettings(settings, payload.Settings); Tools.AutoPopulateSettings(settings, payload.Settings);
// Logger.Instance.LogMessage(TracingLevel.INFO, JsonConvert.SerializeObject(settings)); // Logger.Instance.LogMessage(TracingLevel.INFO, JsonConvert.SerializeObject(settings));
ClipTrimClient.Instance.PlayClip(metadata); ClipTrimClient.Instance.PlayClip(metadata);
//try //try
//{ //{
// WavPlayer.Instance.Play(settings.Path, GlobalSettings.Instance.OutputDevice, settings.Volume, settings.PlayType == "Play/Overlap" ? WavPlayer.PlayMode.PlayOverlap : WavPlayer.PlayMode.PlayStop); // WavPlayer.Instance.Play(settings.Path, GlobalSettings.Instance.OutputDevice, settings.Volume, settings.PlayType == "Play/Overlap" ? WavPlayer.PlayMode.PlayOverlap : WavPlayer.PlayMode.PlayStop);
//} //}
//catch //catch
//{ //{
//} //}
} }
public override void KeyReleased(KeyPayload payload) { public override void KeyReleased(KeyPayload payload) {
} }
public override void OnTick() {
CheckFile(); public override void OnTick() {
} CheckFile();
}
public override async void ReceivedSettings(ReceivedSettingsPayload payload)
{ public override async void ReceivedSettings(ReceivedSettingsPayload payload)
Logger.Instance.LogMessage(TracingLevel.INFO, "Player rec settings"); {
Tools.AutoPopulateSettings(settings, payload.Settings); //Logger.Instance.LogMessage(TracingLevel.INFO, "Player rec settings");
GlobalSettings.Instance.SetFileOptionsCurrentProfile(settings.Path, new FileEntry() { Playtype = settings.PlayType, Volume = settings.Volume }); Tools.AutoPopulateSettings(settings, payload.Settings);
await Connection.SetGlobalSettingsAsync(JObject.FromObject(GlobalSettings.Instance)); GlobalSettings.Instance.SetFileOptionsCurrentProfile(settings.Path, new FileEntry() { Playtype = settings.PlayType, Volume = settings.Volume });
//SaveSettings(); await Connection.SetGlobalSettingsAsync(JObject.FromObject(GlobalSettings.Instance));
//CheckFile(); //SaveSettings();
} //CheckFile();
}
public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload) {
//Logger.Instance.LogMessage(TracingLevel.INFO, "ReceivedGlobalSettings"); public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload) {
//Logger.Instance.LogMessage(TracingLevel.INFO, "ReceivedGlobalSettings");
if (payload.Settings == null || payload.Settings.Count == 0)
{ if (payload.Settings == null || payload.Settings.Count == 0)
var inst = GlobalSettings.Instance; {
//GlobalSettingsManager.Instance.SetGlobalSettings(JObject.FromObject(inst)); var inst = GlobalSettings.Instance;
} //GlobalSettingsManager.Instance.SetGlobalSettings(JObject.FromObject(inst));
else }
{ else
GlobalSettings.Instance = payload.Settings.ToObject<GlobalSettings>(); {
} GlobalSettings.Instance = payload.Settings.ToObject<GlobalSettings>();
}
//CheckFile();
} //CheckFile();
}
#region Private Methods
#region Private Methods
private Task SaveSettings()
{ private Task SaveSettings()
return Connection.SetSettingsAsync(JObject.FromObject(settings)); {
} return Connection.SetSettingsAsync(JObject.FromObject(settings));
}
#endregion
} #endregion
}
} }

View File

@ -1,167 +1,165 @@
using BarRaider.SdTools; using BarRaider.SdTools;
using BarRaider.SdTools.Wrappers; using BarRaider.SdTools.Wrappers;
using ClipTrimDotNet.Client; using ClipTrimDotNet.Client;
using NAudio.CoreAudioApi.Interfaces; using Newtonsoft.Json;
using NAudio.Wave; using Newtonsoft.Json.Linq;
using Newtonsoft.Json; using System;
using Newtonsoft.Json.Linq; using System.Collections.Generic;
using System; using System.Drawing;
using System.Collections.Generic; using System.Dynamic;
using System.Drawing; using System.IO;
using System.Dynamic; using System.Linq;
using System.IO; using System.Text;
using System.Linq; using System.Threading.Tasks;
using System.Text;
using System.Threading.Tasks; namespace ClipTrimDotNet
{
namespace ClipTrimDotNet public class DataSourceItem
{ {
public class DataSourceItem public string label { get; set; }
{ public string value { get; set; }
public string label { get; set; } }
public string value { get; set; } [PluginActionId("com.michal-courson.cliptrim.profile-switcher")]
} public class ProfileSwitcher : KeypadBase
[PluginActionId("com.michal-courson.cliptrim.profile-switcher")] {
public class ProfileSwitcher : KeypadBase private class PluginSettings
{ {
private class PluginSettings public static PluginSettings CreateDefaultSettings()
{ {
public static PluginSettings CreateDefaultSettings() PluginSettings instance = new PluginSettings();
{ instance.ProfileName = null;
PluginSettings instance = new PluginSettings(); return instance;
instance.ProfileName = null; }
return instance;
} [JsonProperty(PropertyName = "profileName")]
public string? ProfileName { get; set; }
[JsonProperty(PropertyName = "profileName")] }
public string? ProfileName { get; set; }
} #region Private Members
#region Private Members private PluginSettings settings;
private PluginSettings settings; #endregion
public ProfileSwitcher(SDConnection connection, InitialPayload payload) : base(connection, payload)
#endregion {
public ProfileSwitcher(SDConnection connection, InitialPayload payload) : base(connection, payload) if (payload.Settings == null || payload.Settings.Count == 0)
{ {
if (payload.Settings == null || payload.Settings.Count == 0) this.settings = PluginSettings.CreateDefaultSettings();
{ SaveSettings();
this.settings = PluginSettings.CreateDefaultSettings(); }
SaveSettings(); else
} {
else this.settings = payload.Settings.ToObject<PluginSettings>();
{ }
this.settings = payload.Settings.ToObject<PluginSettings>(); GlobalSettingsManager.Instance.RequestGlobalSettings();
} Connection.OnSendToPlugin += Connection_OnSendToPlugin;
GlobalSettingsManager.Instance.RequestGlobalSettings(); SetTitle();
Connection.OnSendToPlugin += Connection_OnSendToPlugin; }
SetTitle();
} private async void SetTitle()
{
private async void SetTitle()
{ await Connection.SetTitleAsync(settings.ProfileName + " B");
}
await Connection.SetTitleAsync(settings.ProfileName + " A");
} private async void Connection_OnSendToPlugin(object sender, SDEventReceivedEventArgs<BarRaider.SdTools.Events.SendToPlugin> e)
{
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")
//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");
//string basePath = "C:\\Users\\mickl\\Music\\clips"; var files = ClipTrimClient.Instance.GetCollectionNames();
//var files = Directory.GetDirectories(basePath, "*", SearchOption.TopDirectoryOnly).Select(x => Path.GetFileNameWithoutExtension(x)).Where(x => x != "original"); var items = files.Select(x => new DataSourceItem { label = x, value = x});
var files = ClipTrimClient.Instance.GetCollectionNames(); var obj = new JObject();
var items = files.Select(x => new DataSourceItem { label = x, value = x}); obj["event"] = "getProfiles";
var obj = new JObject(); obj["items"] = JArray.FromObject(items);
obj["event"] = "getProfiles"; //Logger.Instance.LogMessage(TracingLevel.INFO, "get profiles return " + JsonConvert.SerializeObject(obj));
obj["items"] = JArray.FromObject(items); await Connection.SendToPropertyInspectorAsync(obj);
//Logger.Instance.LogMessage(TracingLevel.INFO, "get profiles return " + JsonConvert.SerializeObject(obj)); }
await Connection.SendToPropertyInspectorAsync(obj); //if (e.Event.Payload["event"].ToString() == "getOutputDevices")
} //{
if (e.Event.Payload["event"].ToString() == "getOutputDevices") // List<WaveOutCapabilities> devices = new List<WaveOutCapabilities>();
{ // for (int n = -1; n < WaveOut.DeviceCount; n++)
List<WaveOutCapabilities> devices = new List<WaveOutCapabilities>(); // {
for (int n = -1; n < WaveOut.DeviceCount; n++) // var caps = WaveOut.GetCapabilities(n);
{ // devices.Add(caps);
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();
var items = devices.Select(x => new DataSourceItem { label = x.ProductName, value = x.ProductName }); // obj["event"] = "getOutputDevices";
var obj = new JObject(); // obj["items"] = JArray.FromObject(items);
obj["event"] = "getOutputDevices"; // //Logger.Instance.LogMessage(TracingLevel.INFO, "get devices return " + JsonConvert.SerializeObject(obj));
obj["items"] = JArray.FromObject(items); // await Connection.SendToPropertyInspectorAsync(obj);
//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)
{
private void Connection_OnTitleParametersDidChange(object sender, SDEventReceivedEventArgs<BarRaider.SdTools.Events.TitleParametersDidChange> e) }
{
} public override void Dispose()
{
public override void Dispose() Connection.OnTitleParametersDidChange -= Connection_OnTitleParametersDidChange;
{ //Logger.Instance.LogMessage(TracingLevel.INFO, $"Destructor called");
Connection.OnTitleParametersDidChange -= Connection_OnTitleParametersDidChange; }
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Destructor called");
} public override async void KeyPressed(KeyPayload payload)
{
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, "KeyPressed"); //Logger.Instance.LogMessage(TracingLevel.INFO, JsonConvert.SerializeObject(GlobalSettings.Instance));
//Logger.Instance.LogMessage(TracingLevel.INFO, JsonConvert.SerializeObject(settings)); ClipTrimClient.Instance.SetSelectedCollectionByName(settings.ProfileName);
//Logger.Instance.LogMessage(TracingLevel.INFO, JsonConvert.SerializeObject(GlobalSettings.Instance)); GlobalSettings.Instance.SetCurrentProfile(settings.ProfileName);
ClipTrimClient.Instance.SetSelectedCollectionByName(settings.ProfileName); //Logger.Instance.LogMessage(TracingLevel.INFO, JsonConvert.SerializeObject(GlobalSettings.Instance));
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");
await Connection.SetGlobalSettingsAsync(JObject.FromObject(GlobalSettings.Instance)); }
await Connection.SwitchProfileAsync("ClipTrim");
} public override void KeyReleased(KeyPayload payload)
{
public override void KeyReleased(KeyPayload payload)
{ }
} public override void OnTick()
{
public override void OnTick() SetTitle();
{ }
SetTitle();
} public override void ReceivedSettings(ReceivedSettingsPayload payload)
{
public override void ReceivedSettings(ReceivedSettingsPayload payload) Tools.AutoPopulateSettings(settings, payload.Settings);
{ SaveSettings();
Tools.AutoPopulateSettings(settings, payload.Settings); //tTitle();
SaveSettings(); //CheckFile();
//tTitle(); }
//CheckFile();
} public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload)
{
public override void ReceivedGlobalSettings(ReceivedGlobalSettingsPayload payload) //Logger.Instance.LogMessage(TracingLevel.INFO, "ReceivedGlobalSettings");
{
//Logger.Instance.LogMessage(TracingLevel.INFO, "ReceivedGlobalSettings"); if (payload.Settings == null || payload.Settings.Count == 0)
{
if (payload.Settings == null || payload.Settings.Count == 0) var inst = GlobalSettings.Instance;
{ //GlobalSettingsManager.Instance.SetGlobalSettings(JObject.FromObject(inst));
var inst = GlobalSettings.Instance; }
//GlobalSettingsManager.Instance.SetGlobalSettings(JObject.FromObject(inst)); else
} {
else GlobalSettings.Instance = payload.Settings.ToObject<GlobalSettings>();
{ }
GlobalSettings.Instance = payload.Settings.ToObject<GlobalSettings>();
} //CheckFile();
}
//CheckFile();
} #region Private Methods
#region Private Methods private Task SaveSettings()
{
private Task SaveSettings() return Connection.SetSettingsAsync(JObject.FromObject(settings));
{ }
return Connection.SetSettingsAsync(JObject.FromObject(settings));
} #endregion
}
#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,34 @@
<!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 label="Base Path">
<sdpi-file setting="basePath" <sdpi-file setting="basePath"
global="true" global="true"
webkitdirectory webkitdirectory
directory directory
multiple></sdpi-file> multiple></sdpi-file>
</sdpi-item> </sdpi-item>
<sdpi-item label="Output Device"> <sdpi-item label="Output Device">
<sdpi-select setting="outputDevice" <sdpi-select setting="outputDevice"
global="true" global="true"
datasource="getOutputDevices" datasource="getOutputDevices"
show-refresh="true" show-refresh="true"
placeholder="Select an Ouput Device"></sdpi-select> placeholder="Select an Ouput Device"></sdpi-select>
</sdpi-item> </sdpi-item>
</body> </body>
</html> </html>

View File

@ -1,225 +1,225 @@
using System; //using System;
using System.Collections.Concurrent; //using System.Collections.Concurrent;
using System.Collections.Generic; //using System.Collections.Generic;
using System.Linq; //using System.Linq;
using System.ServiceModel.Security; //using System.ServiceModel.Security;
using System.Threading.Tasks; //using System.Threading.Tasks;
using BarRaider.SdTools; //using BarRaider.SdTools;
using NAudio.Wave; //using NAudio.Wave;
using NAudio.Wave.SampleProviders; //using NAudio.Wave.SampleProviders;
using Newtonsoft.Json; //using Newtonsoft.Json;
class CachedSound //class CachedSound
{ //{
public byte[] AudioData { get; private set; } // public byte[] AudioData { get; private set; }
public WaveFormat WaveFormat { get; private set; } // public WaveFormat WaveFormat { get; private set; }
public CachedSound(string audioFileName) // public CachedSound(string audioFileName)
{ // {
using (var audioFileReader = new AudioFileReader(audioFileName)) // using (var audioFileReader = new AudioFileReader(audioFileName))
{ // {
// TODO: could add resampling in here if required // // TODO: could add resampling in here if required
WaveFormat = audioFileReader.WaveFormat; // WaveFormat = audioFileReader.WaveFormat;
var wholeFile = new List<byte>((int)(audioFileReader.Length)); // var wholeFile = new List<byte>((int)(audioFileReader.Length));
var readBuffer = new byte[audioFileReader.WaveFormat.SampleRate * audioFileReader.WaveFormat.Channels*4]; // var readBuffer = new byte[audioFileReader.WaveFormat.SampleRate * audioFileReader.WaveFormat.Channels*4];
int samplesRead; // int samplesRead;
while ((samplesRead = audioFileReader.Read(readBuffer, 0, readBuffer.Length)) > 0) // while ((samplesRead = audioFileReader.Read(readBuffer, 0, readBuffer.Length)) > 0)
{ // {
wholeFile.AddRange(readBuffer.Take(samplesRead)); // wholeFile.AddRange(readBuffer.Take(samplesRead));
} // }
AudioData = wholeFile.ToArray(); // AudioData = wholeFile.ToArray();
} // }
//Logger.Instance.LogMessage(TracingLevel.INFO, $"AudioData Length {AudioData.Length}"); // //Logger.Instance.LogMessage(TracingLevel.INFO, $"AudioData Length {AudioData.Length}");
} // }
} //}
class CachedSoundSampleProvider : IWaveProvider //class CachedSoundSampleProvider : IWaveProvider
{ //{
private readonly CachedSound cachedSound; // private readonly CachedSound cachedSound;
private long position; // private long position;
~CachedSoundSampleProvider() { // ~CachedSoundSampleProvider() {
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Cache destructor"); // //Logger.Instance.LogMessage(TracingLevel.INFO, $"Cache destructor");
} // }
public CachedSoundSampleProvider(CachedSound cachedSound) // public CachedSoundSampleProvider(CachedSound cachedSound)
{ // {
this.cachedSound = cachedSound; // this.cachedSound = cachedSound;
position = 0; // position = 0;
} // }
public int Read(byte[] buffer, int offset, int count) // public int Read(byte[] buffer, int offset, int count)
{ // {
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Read1 byte"); // //Logger.Instance.LogMessage(TracingLevel.INFO, $"Read1 byte");
var availableSamples = cachedSound.AudioData.Length - position; // var availableSamples = cachedSound.AudioData.Length - position;
var samplesToCopy = Math.Min(availableSamples, count); // var samplesToCopy = Math.Min(availableSamples, count);
try // try
{ // {
//Logger.Instance.LogMessage(TracingLevel.INFO, $"{cachedSound.AudioData.GetType()} {buffer.GetType()}"); // //Logger.Instance.LogMessage(TracingLevel.INFO, $"{cachedSound.AudioData.GetType()} {buffer.GetType()}");
Array.Copy(cachedSound.AudioData, position, buffer, offset, samplesToCopy); // Array.Copy(cachedSound.AudioData, position, buffer, offset, samplesToCopy);
} // }
catch (Exception ex) // catch (Exception ex)
{ // {
//Logger.Instance.LogMessage(TracingLevel.INFO, $"{ex.ToString()}"); // //Logger.Instance.LogMessage(TracingLevel.INFO, $"{ex.ToString()}");
} // }
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Read3"); // //Logger.Instance.LogMessage(TracingLevel.INFO, $"Read3");
position += samplesToCopy; // position += samplesToCopy;
//Logger.Instance.LogMessage(TracingLevel.INFO, $"Sending {samplesToCopy} samples"); // //Logger.Instance.LogMessage(TracingLevel.INFO, $"Sending {samplesToCopy} samples");
return (int)samplesToCopy; // return (int)samplesToCopy;
} // }
public WaveFormat WaveFormat => cachedSound.WaveFormat; // public WaveFormat WaveFormat => cachedSound.WaveFormat;
} //}
public class WavPlayer //public class WavPlayer
{ //{
private static WavPlayer? instance; // private static WavPlayer? instance;
public static WavPlayer Instance // public static WavPlayer Instance
{ // {
get // get
{ // {
instance ??= new WavPlayer(); // instance ??= new WavPlayer();
return instance; // return instance;
} // }
} // }
public enum PlayMode // public enum PlayMode
{ // {
PlayOverlap, // PlayOverlap,
PlayStop // PlayStop
} // }
private readonly ConcurrentDictionary<string, List<Tuple<WaveOutEvent, IWaveProvider>>> _activePlayers; // private readonly ConcurrentDictionary<string, List<Tuple<WaveOutEvent, IWaveProvider>>> _activePlayers;
public WavPlayer() // public WavPlayer()
{ // {
_activePlayers = new ConcurrentDictionary<string, List<Tuple<WaveOutEvent, IWaveProvider>>>(); // _activePlayers = new ConcurrentDictionary<string, List<Tuple<WaveOutEvent, IWaveProvider>>>();
} // }
public void Play(string filePath, string id, double volume, PlayMode mode) // public void Play(string filePath, string id, double volume, PlayMode mode)
{ // {
if (string.IsNullOrWhiteSpace(filePath)) // if (string.IsNullOrWhiteSpace(filePath))
throw new ArgumentException("File path cannot be null or empty.", nameof(filePath)); // throw new ArgumentException("File path cannot be null or empty.", nameof(filePath));
if (mode == PlayMode.PlayOverlap) // if (mode == PlayMode.PlayOverlap)
{ // {
PlayWithOverlap(filePath, id, volume); // PlayWithOverlap(filePath, id, volume);
} // }
else if (mode == PlayMode.PlayStop) // else if (mode == PlayMode.PlayStop)
{ // {
PlayWithStop(filePath, id, volume); // PlayWithStop(filePath, id, volume);
} // }
else // else
{ // {
throw new ArgumentOutOfRangeException(nameof(mode), "Invalid play mode specified."); // throw new ArgumentOutOfRangeException(nameof(mode), "Invalid play mode specified.");
} // }
} // }
private void PlayWithOverlap(string filePath, string id, double volume) // private void PlayWithOverlap(string filePath, string id, double volume)
{ // {
try // try
{ // {
//Logger.Instance.LogMessage(TracingLevel.INFO, "Play overlap"); // //Logger.Instance.LogMessage(TracingLevel.INFO, "Play overlap");
var player = CreatePlayer(filePath, id); // var player = CreatePlayer(filePath, id);
if (!_activePlayers.ContainsKey(filePath)) // if (!_activePlayers.ContainsKey(filePath))
{ // {
_activePlayers[filePath] = new List<Tuple<WaveOutEvent, IWaveProvider>>(); // _activePlayers[filePath] = new List<Tuple<WaveOutEvent, IWaveProvider>>();
} // }
_activePlayers[filePath].Add(player); // _activePlayers[filePath].Add(player);
player.Item1.Volume = (float)volume; // player.Item1.Volume = (float)volume;
player.Item1.Play(); // player.Item1.Play();
} // }
catch(Exception ex) // catch(Exception ex)
{ // {
//Logger.Instance.LogMessage(TracingLevel.INFO, ex.ToString()); // //Logger.Instance.LogMessage(TracingLevel.INFO, ex.ToString());
} // }
//var playersToDispose = _activePlayers[filePath].Where(x => x.Item1.PlaybackState == PlaybackState.Stopped).ToList(); // //var playersToDispose = _activePlayers[filePath].Where(x => x.Item1.PlaybackState == PlaybackState.Stopped).ToList();
//foreach (var p in playersToDispose) // //foreach (var p in playersToDispose)
//{ // //{
// p.Item1.Stop(); // // p.Item1.Stop();
// p.Item1.Dispose(); // // p.Item1.Dispose();
//} // //}
//_activePlayers[filePath].RemoveAll(x => x.Item1.PlaybackState == PlaybackState.Stopped); // //_activePlayers[filePath].RemoveAll(x => x.Item1.PlaybackState == PlaybackState.Stopped);
} // }
private void PlayWithStop(string filePath, string id, double volume) // private void PlayWithStop(string filePath, string id, double volume)
{ // {
if (_activePlayers.TryGetValue(filePath, out var players)) // if (_activePlayers.TryGetValue(filePath, out var players))
{ // {
// Stop and dispose all current players for this file // // Stop and dispose all current players for this file
if (players.Any(x => x.Item1.PlaybackState == PlaybackState.Playing)) // if (players.Any(x => x.Item1.PlaybackState == PlaybackState.Playing))
{ // {
var playersToDispose = players.ToList(); // var playersToDispose = players.ToList();
foreach (var player in playersToDispose) // foreach (var player in playersToDispose)
{ // {
player.Item1.Stop(); // player.Item1.Stop();
player.Item1.Dispose(); // player.Item1.Dispose();
} // }
} // }
else // else
{ // {
PlayWithOverlap(filePath, id, volume); // PlayWithOverlap(filePath, id, volume);
} // }
} // }
else // else
{ // {
// Start a new player // // Start a new player
PlayWithOverlap(filePath, id, volume); // PlayWithOverlap(filePath, id, volume);
} // }
_activePlayers[filePath].RemoveAll(x => x.Item1.PlaybackState == PlaybackState.Stopped); // _activePlayers[filePath].RemoveAll(x => x.Item1.PlaybackState == PlaybackState.Stopped);
} // }
private Tuple<WaveOutEvent, IWaveProvider> CreatePlayer(string filePath, string name) // private Tuple<WaveOutEvent, IWaveProvider> CreatePlayer(string filePath, string name)
{ // {
var reader = new CachedSoundSampleProvider(new CachedSound(filePath)); // var reader = new CachedSoundSampleProvider(new CachedSound(filePath));
//var reader = new AudioFileReader(filePath); // //var reader = new AudioFileReader(filePath);
int number = -1; // int number = -1;
for (int i = 0; i < WaveOut.DeviceCount; ++i) // for (int i = 0; i < WaveOut.DeviceCount; ++i)
{ // {
if (WaveOut.GetCapabilities(i).ProductName == name) // if (WaveOut.GetCapabilities(i).ProductName == name)
{ // {
number = i; // number = i;
} // }
} // }
var player = new WaveOutEvent() { DeviceNumber = number }; // var player = new WaveOutEvent() { DeviceNumber = number };
player.Init(reader); // player.Init(reader);
return new Tuple<WaveOutEvent, IWaveProvider>(player, reader); // return new Tuple<WaveOutEvent, IWaveProvider>(player, reader);
} // }
private void CleanupPlayer(string filePath) // private void CleanupPlayer(string filePath)
{ // {
if (_activePlayers.TryGetValue(filePath, out var players)) // if (_activePlayers.TryGetValue(filePath, out var players))
{ // {
var playersToDispose = players.ToList(); // var playersToDispose = players.ToList();
foreach (var p in playersToDispose) // foreach (var p in playersToDispose)
{ // {
p.Item1.Stop(); // p.Item1.Stop();
p.Item1.Dispose(); // p.Item1.Dispose();
} // }
} // }
_activePlayers[filePath].RemoveAll(x => x.Item1.PlaybackState == PlaybackState.Stopped); // _activePlayers[filePath].RemoveAll(x => x.Item1.PlaybackState == PlaybackState.Stopped);
} // }
public void StopAll() // public void StopAll()
{ // {
foreach (var players in _activePlayers.Values) // foreach (var players in _activePlayers.Values)
{ // {
var playersToDispose = players.ToList(); // var playersToDispose = players.ToList();
foreach (var player in playersToDispose) // foreach (var player in playersToDispose)
{ // {
player.Item1.Stop(); // player.Item1.Stop();
player.Item1.Dispose(); // player.Item1.Dispose();
} // }
players.Clear(); // players.Clear();
} // }
_activePlayers.Clear(); // _activePlayers.Clear();
} // }
} //}

View File

@ -1,62 +1,62 @@
{ {
"Actions": [ "Actions": [
{ {
"Icon": "Images/icon", "Icon": "Images/icon",
"Name": "Player", "Name": "Player",
"States": [ "States": [
{ {
"Image": "Images/pluginAction", "Image": "Images/pluginAction",
"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/icon",
"Name": "Profile Switcher", "Name": "Profile Switcher",
"States": [ "States": [
{ {
"Image": "Images/pluginAction", "Image": "Images/pluginAction",
"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 sub folder to use and opens effect profile",
"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", "Author": "Michal Courson",
"Name": "ClipTrimDotNet", "Name": "ClipTrimDotNet",
"Description": "Pairs with cliptrim for easy voice recording and trimming", "Description": "Pairs with cliptrim for easy voice recording and trimming",
"Icon": "Images/pluginIcon", "Icon": "Images/pluginIcon",
"Version": "0.1.0.0", "Version": "0.1.0.0",
"CodePath": "ClipTrimDotNet.exe", "CodePath": "ClipTrimDotNet.exe",
"Category": "ClipTrimDotNet", "Category": "ClipTrimDotNet",
"CategoryIcon": "Images/categoryIcon", "CategoryIcon": "Images/categoryIcon",
"UUID": "com.michal-courson.cliptrim", "UUID": "com.michal-courson.cliptrim",
"OS": [ "OS": [
{ {
"Platform": "windows", "Platform": "windows",
"MinimumVersion": "10" "MinimumVersion": "10"
} }
], ],
"SDKVersion": 2, "SDKVersion": 2,
"Software": { "Software": {
"MinimumVersion": "6.4" "MinimumVersion": "6.4"
}, },
"Profiles": [ "Profiles": [
{ {
"Name": "ClipTrim", "Name": "ClipTrim",
"DeviceType": 0, "DeviceType": 0,
"Readonly": false, "Readonly": false,
"DontAutoSwitchWhenInstalled": 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>