mirror of
https://github.com/litruv/AudioSort.git
synced 2026-07-24 02:36:01 +10:00
Initial
This commit is contained in:
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
out/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
package-lock.json
|
||||||
219
TEST_README.md
Normal file
219
TEST_README.md
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
# AudioSort Test Suite
|
||||||
|
|
||||||
|
Comprehensive test suite for AudioSort application using real WAV files with embedded metadata.
|
||||||
|
|
||||||
|
## Running Tests
|
||||||
|
|
||||||
|
### Run all tests
|
||||||
|
```powershell
|
||||||
|
npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run specific test suites
|
||||||
|
```powershell
|
||||||
|
# Tag Service tests (metadata read/write)
|
||||||
|
npm run test:tag
|
||||||
|
|
||||||
|
# Library Service tests (scanning, organizing, file operations)
|
||||||
|
npm run test:library
|
||||||
|
|
||||||
|
# Search Service tests (fuzzy search, filters)
|
||||||
|
node --import tsx --test src/test/SearchService.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run individual test files
|
||||||
|
```powershell
|
||||||
|
node --import tsx --test src/test/DatabaseService.test.ts
|
||||||
|
node --import tsx --test src/test/TagService.test.ts
|
||||||
|
node --import tsx --test src/test/LibraryService.test.ts
|
||||||
|
node --import tsx --test src/test/SearchService.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Coverage
|
||||||
|
|
||||||
|
### DatabaseService Tests (20+ tests)
|
||||||
|
- **File Management**: upsertFile, listFiles, getFileById, updateFileLocation, deleteFile
|
||||||
|
- **Tagging Operations**: updateTagging, updateCustomName
|
||||||
|
- **Category Management**: upsertCategory, getCategoryById, listCategories
|
||||||
|
- **Duplicate Detection**: listDuplicateGroups by checksum
|
||||||
|
- **Settings Persistence**: setSetting, getSettings
|
||||||
|
- Edge cases: Missing files, null values, duplicate checksums
|
||||||
|
|
||||||
|
### TagService Tests (14 tests)
|
||||||
|
- **readMetadata**: Reading author, copyright, title, rating from WAV files
|
||||||
|
- **writeMetadataOnly**: Writing metadata to WAV INFO chunks
|
||||||
|
- **applyTagging**: Updating tags/categories in database and WAV files
|
||||||
|
- Edge cases: Missing metadata, corrupted files, empty values, normalization
|
||||||
|
|
||||||
|
### LibraryService Tests (20+ tests)
|
||||||
|
- **scanLibrary**: File discovery, metadata extraction, temp file cleanup
|
||||||
|
- **listFiles**: File listing with metadata
|
||||||
|
- **renameFile**: File renaming on disk and in database
|
||||||
|
- **moveFile**: Moving files between directories
|
||||||
|
- **updateCustomName**: Custom name management
|
||||||
|
- **updateFileMetadata**: Metadata updates without organizing
|
||||||
|
- **readFileMetadata**: Reading embedded WAV metadata
|
||||||
|
- **listMetadataSuggestions**: Aggregating author/copyright suggestions
|
||||||
|
- **deleteFiles**: File deletion from disk and database
|
||||||
|
- **cleanupTempFiles**: Orphaned temp file recovery
|
||||||
|
|
||||||
|
### SearchService Tests (13 tests)
|
||||||
|
- **search**: Fuzzy search by filename, tags, path
|
||||||
|
- **Advanced filters**: author:, copyright: field-specific searches
|
||||||
|
- **Metadata caching**: Performance optimization
|
||||||
|
- **rebuildIndex**: Index updates after database changes
|
||||||
|
- Edge cases: Empty queries, typos, no matches
|
||||||
|
|
||||||
|
## Test Data
|
||||||
|
|
||||||
|
Tests use the `TestWavGenerator` class to create real WAV files with:
|
||||||
|
- Configurable duration, sample rate, bit depth
|
||||||
|
- Embedded metadata (author, copyright, title, rating)
|
||||||
|
- Tags and UCS categories
|
||||||
|
- Sine wave audio data (440 Hz)
|
||||||
|
|
||||||
|
Example test file creation:
|
||||||
|
```typescript
|
||||||
|
await TestWavGenerator.writeTestWav('test.wav', {
|
||||||
|
duration: 1,
|
||||||
|
sampleRate: 44100,
|
||||||
|
bitDepth: 16,
|
||||||
|
author: 'Test Author',
|
||||||
|
copyright: 'Test Copyright',
|
||||||
|
title: 'Test Title',
|
||||||
|
rating: 3,
|
||||||
|
tags: ['test', 'audio'],
|
||||||
|
categories: ['AMBNatr']
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Isolation
|
||||||
|
|
||||||
|
Each test suite:
|
||||||
|
1. Creates temporary database and library directories
|
||||||
|
2. Generates fresh test WAV files
|
||||||
|
3. Initializes services with isolated instances
|
||||||
|
4. Cleans up all resources after tests complete
|
||||||
|
|
||||||
|
Temporary files are created in `test-data/` and automatically removed.
|
||||||
|
|
||||||
|
## Testing Strategy
|
||||||
|
|
||||||
|
### Unit Tests
|
||||||
|
- Test individual service methods in isolation
|
||||||
|
- Verify correct behavior with valid inputs
|
||||||
|
- Handle edge cases and error conditions
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
- Test interactions between services (LibraryService + TagService + SearchService)
|
||||||
|
- Verify database updates reflect in search results
|
||||||
|
- Ensure file system operations complete successfully
|
||||||
|
|
||||||
|
### Real Data Tests
|
||||||
|
- Use actual WAV files with embedded metadata
|
||||||
|
- Test metadata round-trip (write then read)
|
||||||
|
- Verify file integrity after operations
|
||||||
|
|
||||||
|
## Extending Tests
|
||||||
|
|
||||||
|
### Adding New Test Cases
|
||||||
|
|
||||||
|
1. **For TagService**:
|
||||||
|
```typescript
|
||||||
|
it('should handle new metadata field', () => {
|
||||||
|
tagService.writeMetadataOnly(testFilePath, {
|
||||||
|
tags: [],
|
||||||
|
categories: [],
|
||||||
|
newField: 'value'
|
||||||
|
});
|
||||||
|
|
||||||
|
const metadata = tagService.readMetadata(testFilePath);
|
||||||
|
assert.strictEqual(metadata.newField, 'value');
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **For LibraryService**:
|
||||||
|
```typescript
|
||||||
|
it('should handle new operation', async () => {
|
||||||
|
await libraryService.scanLibrary();
|
||||||
|
const files = libraryService.listFiles();
|
||||||
|
|
||||||
|
// Perform operation
|
||||||
|
await libraryService.newOperation(files[0].id);
|
||||||
|
|
||||||
|
// Verify results
|
||||||
|
const updated = libraryService.listFiles();
|
||||||
|
assert.ok(/* verification */);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **For SearchService**:
|
||||||
|
```typescript
|
||||||
|
it('should support new filter', () => {
|
||||||
|
const results = searchService.search('newfilter:value');
|
||||||
|
|
||||||
|
assert.ok(results.length > 0);
|
||||||
|
assert.ok(/* filter applied correctly */);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Tests hang or timeout
|
||||||
|
- Check for leaked file handles (ensure database.close() in afterEach)
|
||||||
|
- Verify temp files are being cleaned up
|
||||||
|
- Increase test timeout if needed
|
||||||
|
|
||||||
|
### File not found errors
|
||||||
|
- Ensure library path is created before writing test files
|
||||||
|
- Check file paths use correct separators for OS
|
||||||
|
- Verify temp directory permissions
|
||||||
|
|
||||||
|
### Metadata not persisting
|
||||||
|
- Confirm WAV file format is correct (use WaveFile library)
|
||||||
|
- Check INFO chunk tags are supported
|
||||||
|
- Verify file is written to disk before reading
|
||||||
|
|
||||||
|
### Search tests fail
|
||||||
|
- Rebuild search index after database changes
|
||||||
|
- Clear metadata cache when needed
|
||||||
|
- Check Fuse.js scoring thresholds
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
- Tests create real WAV files (may be slower than mocks)
|
||||||
|
- Each test suite runs independently with fresh data
|
||||||
|
- Cleanup operations ensure no disk space accumulation
|
||||||
|
- Test data directory can be cleared manually if needed: `rm -rf test-data`
|
||||||
|
|
||||||
|
## CI/CD Integration
|
||||||
|
|
||||||
|
Tests can be run in CI/CD pipelines:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# GitHub Actions example
|
||||||
|
- name: Run tests
|
||||||
|
run: npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests require:
|
||||||
|
- Node.js 18+ (for node:test runner)
|
||||||
|
- File system write access
|
||||||
|
- Sufficient disk space for test WAV files (~1-2 MB per test run)
|
||||||
|
|
||||||
|
## Debugging Tests
|
||||||
|
|
||||||
|
Enable verbose output:
|
||||||
|
```powershell
|
||||||
|
NODE_OPTIONS='--test-reporter=spec' npm test
|
||||||
|
```
|
||||||
|
|
||||||
|
Run specific test:
|
||||||
|
```powershell
|
||||||
|
node --import tsx --test --test-name-pattern="should read author" src/test/TagService.test.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Debug with inspector:
|
||||||
|
```powershell
|
||||||
|
node --import tsx --test --inspect-brk src/test/TagService.test.ts
|
||||||
|
```
|
||||||
754
UCS.csv
Normal file
754
UCS.csv
Normal file
@@ -0,0 +1,754 @@
|
|||||||
|
Category,SubCategory,CatID,CatShort,Explanations,Synonyms - Comma Separated
|
||||||
|
AIR,BLOW,AIRBlow,AIR,"Steady air blows, like from a compressed can of air.","Aerate, Aerosol, Air, Airhose, Balloon, Beat, Bellows, Blast, Blow, Blower, Blowgun, Blown, Blowpipe, Blows, Blowtube, Bluff, Carbon, CO2, Compressed, Depressurize, Dioxide, Duster, Exhaust, Flutter, Gust, Helium, Huff, Inflate, Nitrogen, Oxygen, Puff, Puffed, Purge, Release, Sputter, Vent, Waft, Whiff"
|
||||||
|
AIR,BURST,AIRBrst,AIR,"Sharp air releases, pressure releases, a tennis call can popping open, a fire extinguisher","Air, Airbed, Airblast, Airgun, Airhose, Blast, Blowhole, Blowout, Burst, Carbon, Chuff, CO2, Dioxide, Discharge, Explosion, Flash, Gas, Helium, Jet, Kerboom, Nitrogen, Outburst, Oxygen, Poof, Pop, Rush, Seal, Spurt, Surge, Torrent"
|
||||||
|
AIR,HISS,AIRHiss,AIR,"Slow air releases, a flat tire, leak in an air pipe.","Carbon, CO2, Dioxide, Discharge, Exhaust, Expel, Helium, Hissing, Leak, Nitrogen, Oxygen, Purr, Release, Shush, Sibilate, Whistling"
|
||||||
|
AIR,MISC,AIRMisc,AIR,Air sounds not fitting another category in this list.,"Airtight, Airway, Carbon, CO2, Dioxide, Gas, Helium, Inflatable, Intake, Miscellaneous, Nitrogen, Oxygen, Sky, Ventilation"
|
||||||
|
AIR,SUCTION,AIRSuck,AIR,"Air being sucked in, a vacuum sucking in air, the rush of air in a shopvac.","Air, Aspirate, Aspiration, Carbon, CO2, Consume, Dental, Dioxide, Draw, Helium, Hoover, Ingest, Inspiration, Inspire, Intake, Nitrogen, Oxygen, Pull, Pump, Pumps, Siphon, Suck, Suction, Syphon, Syringe, Vac, Vacuity, Vacuum"
|
||||||
|
AIRCRAFT,DOOR,AERODoor,AERO,"Aircraft doors, some possible overlap with other DOOR categories, or VEHICLES-DOOR.","Airplane, Aviation, Boarding, Cabin, Cargo, Cockpit, Emergency, Entrance, Entryway, Exit, Fuselage, Hatch, Helicopter, Jet, Panel, Passenger, Ports, Trapdoor, Wing"
|
||||||
|
AIRCRAFT,HELICOPTER,AEROHeli,AERO,"For all manner of helicopters, gyrocopters.","Aerochopper, Aircraft, Apache, Autogiro, Autogyro, Bird, Blackhawk, Chopper, Choppers, Copter, Copters, Ghetto, Gyrocopter, Gyroplane, Heli, Helichopper, Helicopter, Helicopteron, Helicoptor, Helijet, Helipad, Helipilot, Heliport, Helo, Helos, Huey, Jetcopter, Lift, Medevac, Medivac, Multirotor, Ornithopter, Police, Rotary-Wing, Rotodyne, Rotorcraft, Sar, Sikorsky, Skyhook, Tailwheel, Tiltrotor, Tricopter, Vertical, VTOL, Whirlybird"
|
||||||
|
AIRCRAFT,INTERIOR,AEROInt,AERO,"Interior recordings (mainly complex ambiences) of aircraft, from cockpit interiors, to passenger jet interiors.","737, 747, 777, A310, A330, A350, A380, Aboard, Aeroplane, Airbus, Aircraft, Airliner, Airplane, Aisle, Avionics, Bay, Belly, Bins, Boeing, Bombardier, Bowels, Cabin, Cargo, Cockpit, Compartment, Crew, DC-10, Deck, Fighter, Flight, Fuselage, Galley, Glider, Hold, Inside, Interior, Jet, Jetliner, Jumbo, Lavatory, Learjet, Midflight, Flight, Overhead, Passenger, Zeppelin"
|
||||||
|
AIRCRAFT,JET,AEROJet,AERO,"All commercial and private jet powered aircraft, military jets would go under AIRCRAFT-MILITARY.","737, 747, 777, A310, A330, A350, A380, Aeroplane, Afterburner, Airbus, Aircraft, Airliner, Airplane, Boeing, Bombardier, Cargo, Commercial, DC-10, Jet, Jetliner, Jumbo, Learjet, Passenger, Plane, Private, Ramjet, Regional, Scramjet, Supersonic, Turbojet, Twinjet, Unducted"
|
||||||
|
AIRCRAFT,MECHANISM,AEROMech,AERO,"Mechanical components of aircraft, for example landing gear, or levers and switches.","Actuators, Aerofoil, Aeroplane, Aileron, Ailerons, Airbrake, Aircraft, Airplane, Apron, Arrester, Autopilot, Avionics, Bombsight, Brakes, Cockpit, Column, Control, Cowl, Cowling, Devices, Doors, Elevator, Elevators, Fin, Flap, Flaps, Flight, Gear, Gimbals, Gyroscope, Hook, Hydraulic, Instrument, Landing, Lever, Pedal, Propellers, Reversers, Rudder, Slats, Spoilers, Surfaces, Systems, Throttle, Thrust, Turbines, Yoke"
|
||||||
|
AIRCRAFT,MILITARY,AEROMil,AERO,"Military aircraft, military fighter jets, stealth bombers, but any military aircraft goes here. Also military drones. Military helicopters could go here or in AIRCRAFT-HELICOPTER.","A10, Aeroplane, Air, Aircraft, Airplane, Angels, Army, Attack, Blue, Bogey, Bomber, Combat, Drone, F16, F18, F22, F35, Fighter, Force, Gunship, Interceptor, Jet, Lockheed, Mig, Military, Mustang, Navy, P51, P52, Plane, Reconnaissance, Sortie, Spy, Squadron, Stealth, Strike, Surveillance, Thunderbirds, Trainer, Transport, Warbird, Warplane"
|
||||||
|
AIRCRAFT,MISC,AEROMisc,AERO,Aircraft not fitting another category in this list.,"Aeroplane, Air, Aircraft, Airplane, Balloon, Blimp, Dirigible, Flyer, Flyover, Glider, Hang, Hang-Glider, Hot, Land, Liftoff, Parachute, Piloting, Runway, Ultralight, Zeppelin"
|
||||||
|
AIRCRAFT,PROP,AEROProp,AERO,Aircraft using props as means of propulsion. A propeller airplane.,"Aeroplane, Aircraft, Airplane, Airscrew, Amphibious, Antique, Beechcraft, Biplane, Bombardier, Bushplane, Cesna, Cherokee, Crop, Cub, Duster, Floatplane, Piper, Plane, Prop, Propeller, Propjet, Seaplane, STOL, Stunt, Triplane, Turboprop, Twin-Prop, Vintage"
|
||||||
|
AIRCRAFT,RADIO CONTROLLED,AERORadio,AERO,"Toy hobby radio controlled, UAV, quadcopters, RC jets and RC helicopters.","Aerial, Airplane, Control, Controlled, Drone, Helicopter, Jet, Model, Quadcopters, Radio, RC, Remote, RPA, RPV, Scale, UAS, UAV, Unmanned"
|
||||||
|
AIRCRAFT,ROCKET,AERORckt,AERO,"Jet powered rockets and rocket engines., missiles.","Blastoff, Booster, Hypersonic, ICBM, Jet, Jetpack, Launch, Launchers, Launching, Launchpad, Missile, Missiles, Missle, NASA, Nuclear, Nuke, Orbit, Payload, Propellant, Propelled, Propellent, Ramjet, Ramjets, Retrorocket, Retrorockets, Rocketeer, Rocketeers, Rocketman, Rocketplane, Rocketry, Rocketship, Shuttle, Skyrocket, Soyuz, Space, Spacelab, Spaceman, Spaceplane, Spaceplanes, Spacex, Sputniks, Suborbital, Thruster, Warhead"
|
||||||
|
ALARMS,BELL,ALRMBell,ALRM,"All alarms which use bells, a school bell, fire alarm bell, a railroad crossing bell.","Alarms, Alert, Bank, Burglar, Caution, Clanger, Clock, Crossing, Fire, Notice, Railroad, Reminder, School, Signal, Striker, Tower, Warning"
|
||||||
|
ALARMS,BUZZER,ALRMBuzr,ALRM,"All alarms using buzzers, or buzzers in general even if not technically an alarm, would probably go here. A front door buzzer.","Alarm, Alarms, Alert, Burglar, Buzzing, Caution, Door, Doorkeeper, Doorman, Emergency, Entry, Fire, Front, Game, Home, Hospital, Intercom, Notice, Reminder, Security, Show, Signal, Warning"
|
||||||
|
ALARMS,CLOCK,ALRMClok,ALRM,"Specifically alarm clocks, digital or analog. A bit of overlap with ALARMS-BELL, but clock alarms of all kinds probably best to go here.","Alarm, Alarms, Analog, Clock, Digital, Sleep, Snooze, Timepiece, Timer, Wind"
|
||||||
|
ALARMS,ELECTRONIC,ALRMElec,ALRM,"All electronic alarms, pure electronic tone alarms. Home burglar alarms, smoke detectors, etc. Car alarms would go to VEHICLES-ALARMS.","Alarms, Alert, Button, Car, Carbon, Caution, CO2, Detector, Digital, Home, Intruder, Intrusion, Keycard, Monoxide, Notice, Panic, Programable, Reminder, Security, Signal, Smoke, Timer, Warning"
|
||||||
|
ALARMS,MISC,ALRMMisc,ALRM,Alarm sounds not fitting another category in this list.,"Alarms, Alert, Anti-Theft, Caution, Homing, Miscellaneous, Notice, Reminder, Signal, Warning"
|
||||||
|
ALARMS,SIREN,ALRMSirn,ALRM,"Mechanical sirens, like old klaxons. Vehicle sirens such a police, fire and ambulance to go to VEHICLES-SIREN.","Ahooga, Air, Air-Raid, Alarms, Blare, Civil, Claxon, Defense, Doppler, Hailer, Hooters, Horn, Klaxon, Raid, Siren, Sirenidae, Tornado"
|
||||||
|
AMBIENCE,AIR,AMBAir,AMB,"Quiet exterior 'air' with little activity, like exterior room tones, very simple exterior ambiences. The exterior equivalent of a room tone. If noticable activity find a better fitting category.","Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Calm, Cave, Clear, Clearing, Desert, Field, Forest, Garden, Mountain, Night, Noiselessly, Oasis, Peaceful, Quiet, Sedate, Serene, Silence, Silent, Snowy, Still, Tranquil, Winter"
|
||||||
|
AMBIENCE,ALPINE,AMBAlpn,AMB,"Mountain ambiences, for recordings and ambiences geographically linked to Alpine Environments. Some overlap with AMBIENCE-RURAL.","Alpen, Alpes, Alpin, Alpine, Alpinist, Alpinists, Alps, Andean, Andes, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Craggy, Downhill, Glacial, Highland, Hilly, Himalayas, Icefalls, Jagged, Matterhorn, Meadow, Montane, Montanic, Mountain, Mountaineer, Mountaineering, Mountainous, Mountains, Mountainscape, Mountainside, Mountaintop, Nordic, Pass, Patagonian, Peak, Peaks, Pyrenean, Pyrenees, Resort, Ridge, Rockies, Rocky, Rugged, Ski, Snowcapped, Snowy, Styrian, Subalpine, Summit, Tetons, Timberline, Tirol, Transalpine, Transmontane, Tyrol, Tyrolean, Ultramontane, Upland, Uplandish, Uralian"
|
||||||
|
AMBIENCE,AMUSEMENT,AMBAmus,AMB,"Theme parks and carnivals. Beware of possible rights issues with uncleared music. Carnivals, fairs, festivals. Probably arcades too.","Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Adventure, Arcade, Biopark, Boardwalk, Bouncing, Bumper, Carnival, Carousel, Cars, Casino, Circus, Coaster, County, Course, Disney, Disneyland, Exploratorium, Fair, Fairground, Ferris, Festival, Flags, Funfair, Funhouse, Gamepark, Go-Kart, Golf, Haunted, Hellhouse, House, Merry-Go-Round, Midway, Mini, Mini-Golf, Park, Pinball, Playland, Roller, Six, Six-Flags, Theater, Theme, Tourist, Traveling, Venue, Wally, Water, Waterpark, Wheel, Wonderworld, Zoological"
|
||||||
|
AMBIENCE,BIRDSONG,AMBBird,AMB,"Group songbirds, like a dawn chorus of pretty birds, where the birds are the prominent sound. Single birds would go in BIRDS-SONGBIRD.","Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Bird, Birdcall, Birdcalls, Birdlife, Birdsong, Birdsongs, Cacophony, Cheeping, Chirping, Chirpings, Chirps, Chittering, Chorus, Dawn, Flocks, Morning, Nightbirds, Nightingales, Peeping, Pinewoods, Pretty, Singing, Songbird, Songbirds, Songful, Soundscape, Sparrows, Spring, Susurration, Susurrus, Thrum, Trilling, Tweeting, Twittering, Twitterings, Vocalisations, Warble, Warbling, Warblings, Whistlings"
|
||||||
|
AMBIENCE,CELEBRATION,AMBCele,AMB,"Parties, New Years Eve. Not pure crowds, as that would go in CROWDS, but i.e. exterior city new years. Also see AMBIENCES-AMUSEMENT for something like a fair or festival.","Anniversary, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Awards, Birthday, Birthdays, Blast, Blowout, Carnival, Celebrants, Ceremonials, Ceremonies, Ceremony, Championships, Commemorate, Commemorating, Commemoration, Commemorations, Commemorative, Concelebrate, Congratulating, Decorations, Drunken, Eid, Elation, Enjoyments, Entertainments, Event, Excitement, Excitements, Exhilaration, Extravaganza, Extravaganzas, Exultation, Exulting, Fairs, Feasts, Felicitating, Felicities, Fest, Festival, Festivals, Festiveness, Festivities, Festivity, Fete, Fiesta, Fireworks, Functions, Gala, Galas, Gathering, Gatherings, Graduation, Graduations, Happiness, Joyous, Joyousness, Jubilance, Jubilating, Jubilation, Jubilee, Merriment, Merrymaking, Observance, Observances, Occasion, Occasions, Overjoyed, Parade, Parades, Parties, Party, Partygoers, Partying, Prom, Public, Qing, Quinceañera, Rager, Rave, Reception, Religious, Revellers, Revelling, Revelries, Revelry, Traditional, Traditions, Victories, Wedding, Weddings"
|
||||||
|
AMBIENCE,CONSTRUCTION,AMBCnst,AMB,"Construction scenes with things like jackhammers, cranes, pile drivers, composite construction scene, road construction also.","Architect, Architectural, Architecture, Assembly, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Build, Builder, Building, Buildings, Buildup, Built, Carpentry, Construct, Constructing, Constructive, Constructor, Contracting, Contractor, Demolition, Design, Engineering, Erect, Erection, Foundation, Home, Housing, Infrastructure, Installation, Labour, Manufacture, Planning, Plot, Prefab, Preparation, Project, Projects, Provision, Rebuild, Rebuilding, Reconstruct, Reconstruction, Remodel, Reno, Renovation, Repair, Road, Site, Workmanship, Works, Worksite"
|
||||||
|
AMBIENCE,DESERT,AMBDsrt,AMB,Quiet desert ambiences with appropriate insects and birds. Maybe tumbleweeds.,"Abandon, Arabian, Arid, Aridity, Aridness, Atacama, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Badland, Badlands, Bare, Barren, Bedouin, Bedouins, Cacti, Cactuses, Canyons, Dearth, Desertlike, Desolate, Dry, Dune, Dunes, Dustbowl, Empty, Expanse, Flatlands, Forsake, Gobi, Godforsaken, Grassless, Hilltop, Hot, Hyperarid, Infertile, Inhospitable, Kalahari, Karakum, Lifeless, Lonely, Mirage, Mirages, Mohave, Mojave, Moonscape, Oases, Oasis, Outback, Parched, Playa, Sable, Sagebrush, Sahara, Saharan, Saharawi, Sahraoui, Sahrawi, Sand, Sandstorms, Sandy, Scrublands, Semidesert, Sonoran, Steppes, Sunbaked, Tumbleweed, Wadi, Wadis, Walkabout, Waste, Wasteland, Wastelands, Waterless"
|
||||||
|
AMBIENCE,DESIGNED,AMBDsgn,AMB,Artificial designed ambience. V8.2 adds AMBIENCE-FANTASY and AMBIENCE-SCIFI which may be better fit for most 'designed' categories.,"Artificial, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Constructed, Created, Manufactured"
|
||||||
|
AMBIENCE,EMERGENCY,AMBEmrg,AMB,"Crime scenes, firefighting scenes, accident scenes, with things like firefighters, police, ambulances and medics. Also see VEHICLES-EMERGENCY.","Accident, Assistance, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Burglary, Calamity, Casualty, Catastrophe, Crime, Crises, Crisis, Danger, Disaster, Disasters, Evac, Evacuation, Evacuations, Evidence, Fighting, Fire, Hazard, Hazardous, Help, Incident, Injury, Lifesaving, Mass, Medevac, Medical, Medics, Murder, Natural, Paramedics, Pileup, Plight, Police, Rescue, Resuscitation, Resuscitative, Robbery, Scene, Search, Shooting, SOS, Succor, Triage, Urgence, Urgency, Urgent, Urgently"
|
||||||
|
AMBIENCE,FANTASY,AMBFant,AMB,"Magical or other wordly composite ambiences, soundscapes, think Middle Earth or Pandora.","Adventure, Arthurian, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Castle, Costume, Dream, Dreaming, Dreamland, Dreamlands, Dreamlike, Dreams, Dreamscape, Dreamworld, Dwarven, Earth, Elven, Enchanted, Fairy, Fairyland, Fairytale, Fairytales, Fantasia, Fantasist, Fantasizing, Fantastic, Fantasyland, Fictional, Figment, Forest, Hallucination, Illusory, Imaginary, Imagination, Imaginative, Imaginings, Imitation, Kingdom, Lair, Magical, Middle, Mine, Mirage, Mystical, Mythical, Mythological, Mythology, Mythopoeic, Pandora, Phantasie, Phantasmagorical, Phantasy, Realm, Roleplaying, Surrealism, Surrealist, Surrealistic, Tower, Unreal, Unrealism, Unreality, Utopia, Utopian, Utopianism, Village, Visionary, Whimsical, Wizard"
|
||||||
|
AMBIENCE,FARM,AMBFarm,AMB,"Specifically farm ambiences, with animals, maybe tractors working, chickens, etc. Isolated farm animals would go into ANIMALS-FARM.","Agrarian, Agricultural, Agriculture, Agro, Agronomic, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Aquaculture, Arable, Barn, Breeding, Cattle, Chickens, Corn, Corral, Cow, Crops, Cultivar, Cultivate, Cultivation, Cultured, Dairy, Farmer, Farmland, Farmstead, Goat, Grain, Grange, Grow, Grower, Harvest, Harvesting, Hatchery, Herding, Homestead, Husbandry, Land, Manufacturer, Nurture, Operation, Operational, Orchard, Pastoral, Pasture, Peasant, Pen, Pharm, Pig, Plantation, Planting, Poultry, Produce, Producer, Raise, Ranch, Rancher, Ranching, Rangeland, Rearing, Rural, Sheep, Silo, Stockbreeding, Stocking, Stud, Tillage, Tilling, Tractor, Vineyard"
|
||||||
|
AMBIENCE,FOREST,AMBForst,AMB,"Forest recordings or forest composite files. Trees and birds, maybe wind in trees all together. Some overlap with AMBIENCE-RURAL or AMBIENCE-ALPINE.","Alder, Apple, Ash, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Aspen, Beech, Birch, Boreal, Boxwood, Buckeye, Cedar, Cherry, Chestnut, Conifer, Coniferous, Coppice, Coppices, Copse, Copses, Cypress, Deciduous, Deforestation, Dogwood, Elm, Fir, Forester, Foresters, Forestier, Forestland, Forestlands, Forestry, Foresty, Forrest, Forrests, Greenwood, Grove, Hemlock, Hickory, Larch, Lumbering, Magnolia, Mahogany, Maple, Oak, Old-Growth, Pine, Pines, Plantation, Poplar, Primeval, Rainforest, Rainforests, Redwood, Reforesting, Reliant, Reserve, Sequoia, Spruce, Stands, Stumpage, Sycamore, Taiga, Temperate, Thicket, Timber, Timberland, Tree, Trees, Treetop, Tress, Underbrush, Undergrowth, Understory, Unforested, Vegetation, Weald, Wilderness, Wildfires, Wildland, Wildwood, Willow, Wooded, Woodland, Woodlands, Woodlot, Woods, Woody, Yew"
|
||||||
|
AMBIENCE,GRASSLAND,AMBGras,AMB,"Prairies, meadows, grazing patures, etc. But FARMS has own category if farm animals or human activity are present.","Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Cropland, Croplands, Farmland, Field, Floodplain, Grass, Grasses, Grassy, Grazing, Habitat, Habitats, Lowland, Meadow, Meadowland, Meadows, Moorland, Overgrazed, Pasture, Pastureland, Pastures, Plain, Plains, Prairie, Prairies, Range, Rangeland, Rangelands, Sagebrush, Savanna, Savannah, Scrublands, Semiarid, Semidesert, Seminatural, Shortgrass, Shrubland, Shrublands, Steppe, Subalpine, Tallgrass, Tussock, Unforested, Ungrazed, Uplands, Vegetational"
|
||||||
|
AMBIENCE,HISTORICAL,AMBHist,AMB,"Old West scenes, Roman street scenes. Composite history and ancient ambiences.","Ancestral, Ancient, Anthropological, Antique, Archaeologic, Archaeological, Archeological, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Biblical, Biographical, Castle, Chronological, Chronology, City, Classic, Earlier, Epoch, Epochal, Factual, Folkloric, Former, Geographic, Geographical, Geological, Geopolitical, Ghost, Gold, Greece, Groundbreaking, Heritage, Historian, Historically, Histories, Historiographic, Historiographical, Historique, History, Landmark, Legendary, Literary, Longstanding, Medieval, Monument, Monumental, Monuments, Old, Olden, Origin, Palace, Past, Pedigree, Philosophical, Prehistoric, Prehistorical, Previous, Prior, Pseudohistorical, Pyramid, Real, Retrospective, Rome, Ruin, Rush, Site, Story, Tale, Timeline, Town, Traditional, Village, Vintage, West"
|
||||||
|
AMBIENCE,HITECH,AMBTech,AMB,"Hi-tech machine rooms. Data centers, control rooms. James Bond type lairs. Spaceship interiors.","007, Advanced, Artificial, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Biotechnology, Bond, Center, Control, Cutting-Edge, Cyber, Cyberactive, Cybercentric, Cybercriminal, Cybererotic, Cybergenic, Cyberliterate, Cyberoptimistic, Cyberphysical, Cyberpunky, Cyberqueer, Cyberreal, Cybersavvy, Cybersexy, Data, Digital, Futuristic, High-Tech, Innovative, Intelligence, Intercomputer, Internet, Internetlike, Internetted, James, Lab, Laboratory, Machine, Modern, Multiserver, Multisite, Multiuser, Multiworkstation, Nanotechnology, Online, Research, Robotics, Room, Scientist, Server, Sophisticated, Spy, Technical, Techno, Technological, Technology, Virtual"
|
||||||
|
AMBIENCE,HOSPITAL,AMBHosp,AMB,"Hospital ambiences of all kinds, surgery and emergency rooms too. Possibly veterinarian hospitals too.","Admission, Ambulances, Ambulatory, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Beds, Bellevue, Care, Clinic, Clinica, Clinical, Convalesces, Department, Doctor, Doctors, Emergency, Gynecology, Healthcare, Hosp, Hospice, Hospitalier, Hospitalist, Hospitality, Hospitalization, Hospitalizations, Hospitalized, House, ICU, Infirmaries, Infirmary, Inpatient, Inpatients, Intensive, Laboratory, Maternity, Medevac, Medic, Medical, Mental, Morgue, Mortuary, Neonatal, Nurse, Nurses, Obstetrics, Occupational, Oncology, Operating, Outpatient, Outpatients, Patient, Pediatrics, Pharmacy, Physical, Physician, Radiology, Recuperating, Room, Sanatorium, Sanitarium, Sick, Sickbay, Sickhouse, Surgery, Therapy, Treatment, Unit, Waiting, Ward"
|
||||||
|
AMBIENCE,INDUSTRIAL,AMBInd,AMB,"Industrial warehouses and factories, plants or any ambience with an industrial feeling.","Aerospace, Assembly, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Automotive, Biomedical, Biotechnical, Chemical, Construction, Engineering, Fabrication, Facility, Factories, Factory, Foundry, Heavy, Industrial, Industrialism, Industrialists, Industrialization, Industrialized, Industrializing, Industries, Industry, Institutional, Intellectual, Labour, Line, Machinery, Manufacturing, Mercantile, Metallurgic, Mill, Mining, Park, Petrochem, Petrochemical, Pharmaceutical, Plant, Postindustrial, Power, Press, Printing, Processing, Production, Recycling, Refinery, Sector, Sectoral, Sectors, Site, Steel, Technical, Textile, Warehouse, Worker, Workers, Working, Workshop"
|
||||||
|
AMBIENCE,INSECT,AMBInsc,AMB,"Thick ambiences of insects, cicada and cricket beds mainly. May contain over sounds but main sound being large presence of insects. See also ANIMALS-INSECT for more isolated sounds.","Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Bee, Beehive, Beekeeper, Bug, Chirpy, Cicadas, Cocoon, Crickets, Flies, Fly, Katydids, Locusts, Mosquito, Nest, Nested, Nesting, Pest, Pesticide, Pheromone, Swarm, Wasp"
|
||||||
|
AMBIENCE,LAKESIDE,AMBLake,AMB,"Ambiences relating to lakes, not isolated lapping, but the lake version of AMBIENCE-SEASIDE. See also WATER-LAPPING.","Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Beach, Beachside, Boathouse, Boathouses, Creekside, Dock, Dockside, Fishing, Frontage, Lakefront, Lakehead, Lakes, Lakescape, Lakeshore, Lakeshores, Lakeview, Lakewater, Lapping, Loch, Lochside, Pond, Pondside, Quayside, Reservoir, Riverbank, Riverfront, Riverside, Shore, Shorefront, Shoreland, Shores, Swimming, Waterfront"
|
||||||
|
AMBIENCE,MARKET,AMBMrkt,AMB,"Busy exterior or interior market activity and crowds. Think vibrant international markets in Istanbul or Africa, but any market.","Antique, Art, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Bargain, Bazaar, Bazar, Buy, Buying, Christmas, Commerce, Community, Craft, District, Exchange, Farmers, Fish, Flea, Food, Garage, Grocery, Hawker, Machado, Market, Marketplace, Mart, Mercado, Mercato, Merchandising, Merchant, Merchantable, Merchants, Night, Organic, Public, Purchasing, Sale, Sell, Selling, Shopping, Square, Street, Trade, Trading, Vendor, World"
|
||||||
|
AMBIENCE,MISC,AMBMisc,AMB,Ambience sounds not fitting another category in this list.,"Ambiance, Atmospheres, Atmospherical, Atmospherics, Miscellaneous, Surroundings"
|
||||||
|
AMBIENCE,NAUTICAL,AMBNaut,AMB,"Relating to ships at sea, shipyards, things revolving around ships. (compare to AMBIENCE-SEASIDE). Water only recordings would go to a WATER category.","Aft, Anchorage, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Barnacled, Beachy, Boat, Boating, Boaty, Bow, Buccaneer, Buoy, Buoyage, Deck, Dock, Docks, Harbor, Jetty, Landlubber, Landlubbers, Mapping, Marina, Marine, Mariner, Mariners, Maritime, Mercantile, Midship, Naut, Nautic, Nautique, Naval, Navicular, Navies, Navigable, Navigational, Oceangoing, Oceanic, Oceanographic, Oceanographical, Offshore, Paddling, Pier, Piratic, Piratical, Port, Preppy, Sailor, Sailorly, Sailors, Sailworthy, Seaborne, Seafarer, Seafarers, Seafaring, Seagoing, Seakindly, Seamanlike, Seamanship, Seaward, Seaworthy, Semiseafaring, Ship, Shipboard, Shipbuilding, Shiplike, Shiply, Shipworks, Shipyard, Stern, Waterborne, Wharf, Yacht, Yachting, Yachty"
|
||||||
|
AMBIENCE,OFFICE,AMBOffc,AMB,"Workplaces, offices, people working, typing, telephones. General offices of various sizes.","Administrative, Agency, Agent, Applicant, Appointment, Attorney, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Authority, Branch, Bureau, Business, Career, Chairmanship, Chambers, Clerical, Commission, Commissioner, Committee, Company, Corporate, Corporation, Council, Cubicle, Delegation, Dental, Department, Desk, Directorate, Employment, Firm, Government, Home, Job, Law, Management, Medical, Ministry, Official, Ombudsman, Organization, Phones, Precinct, Premises, Professional, Ringing, Secretariat, Station, Vocation, Work, Workplace, Workspace, Workstation"
|
||||||
|
AMBIENCE,PARK,AMBPark,AMB,"Parks, mostly city or suburban, probably usually with some human activity, city activity. Not pure nature sounds.","Arboretum, Area, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Ballfield, Ballpark, Banff, Botanical, Campground, Campsite, City, Common, Commons, Dog, Esplanade, Gardens, Green, Grounds, Lawn, National, Nature, Parc, Parcs, Parke, Parkland, Path, Pavilion, Picnic, Playground, Plaza, Public, Recreation, Reserve, Safari, Sandbox, Skate, Space, Square, State, Suburban, Tract, Trail, Trails, Urban, Visitor, Visitors, Walking, Wildlife, Zoo"
|
||||||
|
AMBIENCE,PRISON,AMBPrisn,AMB,"Prisons and jails, reverberant inmate walla, jail doors, buzzers, that sort of thing.","Arrest, Attica, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Blockhouse, Brig, Captivity, Cellblock, Cellmate, Center, Clink, Confinement, Correction, Correctional, Corrections, Corrective, County, Criminal, Custodial, Custody, Deprivation, Detained, Detainee, Detainees, Detention, Facility, Federal, Guard, Guardhouse, Gulag, Holding, Imprison, Imprisoned, Imprisonment, Incarcerated, Inmate, Inmates, Institution, Internment, Jail, Jailed, Jailer, Jailers, Jailhouse, Jailing, Jailors, Jails, Juvenile, Juvie, Locked, Lockup, Maximum, Medium, Military, Minimum, Offender, Parole, Penal, Penalty, Penitentiaries, Penitentiary, Prison, Prisoner, Prisoners, Private, Probation, Psychiatric, Punishment, Reformatory, Remand, Seclusion, Security, Sentence, Sentenced, Servitude, Slammer, Solitary, Stockade, Term, Warden, Warders, Yard"
|
||||||
|
AMBIENCE,PROTEST,AMBPrtst,AMB,"Protests, Rallies, Riots. Big mixed ambiences of crowds, shouting, yelling, chanting. Opposed to AMBIENCE-CHEERING.","Activism, Angry, Anti-War, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Argue, Black, Boycott, Boycotting, Boycotts, Change, Chanting, Civil, Climate, Control, Crowd, Defiance, Demonstrate, Demonstrating, Demonstration, Demonstrations, Denounce, Denouncement, Denunciation, Disapproval, Discontent, Displeasure, Dispute, Dissatisfaction, Dissent, Encampment, Environmental, Gun, Heckling, Human, Justice, Labor, LGBTQ, Lives, March, Marches, Matter, Mutiny, Oppose, Opposition, Outrage, Parade, Petition, Picket, Picketer, Picketing, Political, Procession, Protestors, Rallies, Rally, Rallying, Rants, Rebellion, Resist, Resistance, Retaliation, Revolt, Revolution, Rights, Riot, Rioting, Riots, Screaming, Social, Solidarity, Strike, Turmoil, Union, Uprising, Vigil, Yelling"
|
||||||
|
AMBIENCE,PUBLIC PLACE,AMBPubl,AMB,"Except for restaurants, for any other usually indoor public places such as stores, lobbies, hotels, malls, museums. Lots of possible overlap with other categories. Use if not fitting elsewhere.","Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Casino, Center, Cinema, Civic, Coffeeroom, Concessions, Convenience, Concourse, Courthouse, Courtyards, Escape, Fitness, Gallery, Grocery, Gyms, Hostel, Hotel, Hotels, Library, Lobbies, Lunchroom, Mall, Malls, Meeting, Motel, Museum, Museums, Park, Pedestrian, Plaza, Post, Premises, Public, Room, Shopping, Square, Squares, Stores, Supermarket, Theater, Venue"
|
||||||
|
AMBIENCE,RELIGIOUS,AMBRlgn,AMB,"Church and temples, church services, religious gatherings, pilgrimages. Any ambience relating to religion.","Ashram, Atheist, Atheistic, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Basilica, Belief, Buddhism, Buddhist, Cathedral, Chapel, Choir, Church, Churches, Churchgoing, Churchlike, Churchly, Clergy, Clergyman, Cleric, Clerical, Clerics, Communalistic, Confessional, Convent, Coptic, Creed, Cult, Cultic, Cults, Cultural, Denomination, Devout, Dini, Divine, Ecclesiastic, Ecclesiastical, Evangelic, Evangelical, Faith, Faiths, Fundamentalist, Gurdwara, Holy, Interfaith, Interreligious, Irreligious, Jain, Koranic, Mandir, Masjid, Missionaries, Monastery, Monk, Monks, Mosque, Nonsecular, Nuns, Oratory, Orthodox, Pagan, Pagoda, Parochial, Pastor, Pious, Prayer, Preacher, Priest, Priests, Rabbi, Religieuse, Religieux, Religion, Religionist, Religions, Religiosity, Revered, Sacred, Scripture, Scrupulous, Sectarian, Secular, Secularist, Service, Shinto, Shrine, Spiritual, Synagogue, Synodal, Tabernacle, Temple, Theistic, Theocracy, Theocratic, Theologic, Theological, Worship, Zen"
|
||||||
|
AMBIENCE,RESIDENTIAL,AMBHome,AMB,"Something busier than a room tone, an Apartment, a House with some activity from outside etc. Interior with exterior sounds coming through closed or open doors, windows.","Accommodation, Accommodations, Apartment, Apartments, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Brownstone, Buildings, Bungalow, Cabin, Condo, Condominium, Cottage, Domestic, Domicile, Domiciled, Domiciliary, Dormitory, Duplex, Dwellers, Dwelling, Dwellings, Family, Farmhouse, Flat, Flats, Habitation, Habitational, Habitations, Habitative, Home, Homes, Homewards, House, Houseboat, Household, Houses, Housing, Igloo, Inhabitants, Inhabited, Loft, Mansion, Mobile, Multifamily, Occupancy, Penthouse, Premises, Residence, Residences, Residencies, Residency, Resident, Residential, Residentiary, Residents, Residing, Row, Single-family, Studio, Subsistence, Tenants, Townhouse, Triplex, Villa, Yurt"
|
||||||
|
AMBIENCE,RESTAURANT & BAR,AMBRest,AMB,"Restaurants and bars, pubs, places of eating and drinking, mixed crowd with glass clinks or silverware.","Automat, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Bakery, Bar, Beer, Bistro, Brasserie, Brewery, Brewpub, Burger, Cafe, Cafeteria, Canteen, Cantina, Cart, Catering, Chophouse, Club, Cocktail, Coffee, Coffeehouse, Commissary, Creperie, Culinary, Deli, Diet, Dietary, Diner, Diners, Dinery, Disco, Dive, Drive-In, Eateries, Eatery, Eating, Fast-Food, Food, Foodservice, Foodstore, Gastropub, Grillroom, Hookah, Hostelry, Irish, Karaoke, Lounge, Lunch, Luncheon, Luncheonette, Lunchroom, Meal, Microbrewery, Nightclub, Nightspot, Parlor, Pizzeria, Pub, Restaurant, Restaurante, Restauranteur, Restaurantland, Restaurateur, Restaurateurs, Restauration, Resto, Restobar, Saloon, Seafood, Shop, Snack, Speakeasy, Sports, Stall, Steakhouse, Strip, Sushi, Tapas, Taproom, Tavern, Tearoom, Tiki, Trattoria, Waiter, Waitress, Whiskey, Wine"
|
||||||
|
AMBIENCE,ROOM TONE,AMBRoom,AMB,"Simple room tones with little or no activity. Also see now AMBIENCE-RESIDENTIAL if more than just air is present, as it might be a better fit.","Air, Ambient, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Attic, Basement, Bathroom, Bedroom, Conference, Conservatory, Dead, Den, Dining, Garage, Hotel, Kitchen, Library, Living, Lobby, Office, Quiet, Room, Roomtone, Room Tone, Study, Sunroom, Tone, Waiting, Whirr"
|
||||||
|
AMBIENCE,RURAL,AMBRurl,AMB,"Rural and countryside ambiences that don't fit somewhere else in this list. Nature, away from signs of people, overlap with AMBIENCE-ALPINE, DESERT, FOREST, PRAIRIE, etc.","Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Canyon, Coulee, Country, Countryside, Hill, Midwestern, Plateau, Remote, Rolling, Rurale, Rurales, Ruralist, Ruralistic, Ruralness, Rurals, Rustic, Savanna, Scenic, Scrub, Scrubland, Valley"
|
||||||
|
AMBIENCE,SCHOOL,AMBSchl,AMB,"Schools of any age, kindergarten through college, hallways, lockers, voices, school bells.","Academic, Academy, Admission, Assembly, Attendance, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Baccalaureate, Blackboard, Boarding, Cafeteria, Campus, Class, Classroom, College, Collegium, Community, Courses, Curricular, Curriculum, Dormitory, Dropout, Educate, Education, Educational, Elementary, Enrolment, Faculty, Grade, Graduate, Gym, High, Institute, Institution, Instructional, International, Junior, Kindergarten, Learn, Learning, Lesson, Lyceum, Middle, Montessori, Nursery, Polytechnic, Preschool, Primary, Private, Public, Pupil, Recess, Scholar, Scholarly, Scholastic, Schoolchild, Schoolhouse, Schoolroom, Schooltime, Schoolwork, Secondary, Seminar, Seminary, Student, Studies, Studying, Teacher, Teaching, Trade, Tuition, University, Vocational"
|
||||||
|
AMBIENCE,SCIFI,AMBSci,AMB,"A composite sci-fi ambience, a complex background layered with many sci-fi elements. A complex spacehip bridge, or high-tech lab. See also SCIFI for individual elements.","Alien, Anime, Artificial, Asimov, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Beowulf, Center, Chamber, Cheesy, City, Command, Cyberpunk, Dystopian, Extraterrestrial, Fiction, Future, Futuristic, Genetic, Gravity, Hi-Tec, High-Tech, Holographic, Intelligence, Interstellar, Manga, Mars, Martians, Moon, Moonscape, Nanotechnology, Nerds, Planet, Planets, Portal, Sci, Sci-Fi, Science, Singularity, Space, Spaceship, Star, Starwars, Station, Steampunk, Sun, Tech, Teleportation, Teleporter, Time, Travel, Trek, Trekkie, Tribble, Virtual, Wars, Wormhole, Wormholes"
|
||||||
|
AMBIENCE,SEASIDE,AMBSea,AMB,Relating to beach and oceanside scenes with people. (Compare to AMBIENCE-NAUTICAL). Water only recordings would go to a WATER category.,"Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Bathing, Bay, Bayside, Beach, Beaches, Beachfront, Beachline, Beachscape, Beachside, Beachy, Boardwalk, Coast, Coastal, Coastland, Coastline, Coasts, Costal, Cove, Dock, Embankment, Esplanade, Harbor, Harborside, Headland, Inshore, Intertidal, Lighthouse, Marina, Mediterranean, Ocean, Oceanfront, Oceanside, Pier, Promenade, Quay, Quayside, Resort, Sandbeach, Sandy, Sea, Seaboard, Seacliff, Seacoast, Seafront, Seaport, Seascape, Seashore, Seashores, Seaside, Shore, Shoreface, Shorefront, Shoreland, Shoreline, Shoreside, Strand, Surf, Surfside, Tideland, Waterfront, Waterside, Waves, Windswept"
|
||||||
|
AMBIENCE,SPORT,AMBSprt,AMB,"Sporting events, little league baseball to major league. Use CROWDS-SPORT for clean crowd recordings.","Arena, Athletic, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Baseball, Basketball, Boxing, Challenge, Competition, Complex, Court, Cricket, Event, Facility, Field, Football, Gymnasium, Hockey, League, Little, MLB, MMA, NBA, NFL, Practice, Soccer, Sports, Stadium, Tennis, Track, Training, Wrestling"
|
||||||
|
AMBIENCE,SUBURBAN,AMBSubn,AMB,"Suburban streets, average residential neighborhood, not dense like URBAN. Lawnmowers, sprinklers, birds.","Affluent, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Barrio, Bucolic, Burbs, Community, Cul-De-Sac, Development, Exurb, Exurban, Exurbia, Exurbs, Gated, Hamlet, Insular, Interurban, Middleclass, Neighborhood, Outskirts, Residential, Sprawl, Suburb, Suburbanism, Suburbanite, Suburbanites, Suburbia, Tract, Village"
|
||||||
|
AMBIENCE,SWAMP,AMBSwmp,AMB,"A wetland often partially or intermittently covered with water; especially : one dominated by woody vegetation. Marshes, although tecnically different, are included here. Any wetland.","Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Backwater, Backwaters, Bayou, Bayous, Bog, Boggy, Bogs, Bottomland, Bottomlands, Brackish, Everglade, Everglades, Fen, Fenland, Fens, Fenway, Fetid, Flood, Freshwater, Frog, Lagoon, Lowland, Mangrove, Marais, Marsh, Marshes, Marshland, Marshlands, Marshy, Mire, Mires, Moat, Morass, Morasses, Mucky, Mudbank, Muddy, Mudflat, Mudhole, Mudholes, Murk, Muskeg, Pocosin, Quagmire, Quagmires, Quicksand, Riparian, Saltwater, Slough, Sloughs, Swampland, Swamplands, Swampy, Tamarack, Thickets, Waterlogged, Wetland, Wetlands"
|
||||||
|
AMBIENCE,TOWN,AMBTown,AMB,"Something in between a CITY and a SUBURB. Activity, cars, pedestrians, that 'small town' or village sound. May be confusion between this, AMBIENCE-URBAN and AMBIENCE-SUBURBAN.","Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Borough, Boroughs, Burg, Capital, Center, Cities, Ciudad, Commune, Community, Hamlet, Hometown, Local, Localities, Locality, Location, Main, Municipalities, Municipality, Neighborhood, Settlement, Small, Square, Street, Town, Townhall, Townish, Townsfolk, Township, Townspeople, Village, Villages"
|
||||||
|
AMBIENCE,TRAFFIC,AMBTraf,AMB,"Specifically traffic without people. Not for a single car by, but for clean busy traffic, clean highway traffic for example. Rush hour where the sound is predominantly cars.","Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Avenue, Boulevard, Bridge, Bys, Car, Cars, Circle, Congestion, Detour, Expressway, Freeway, Highway, Hour, Intersection, Jam, Lane, Motorway, Overpass, Road, Roads, Roundabout, Route, Rush, Signage, Signaling, Street, Toll, Transportation, Travelling, Underpass, Vehicles, Vehicular, Wash, Washy"
|
||||||
|
AMBIENCE,TRANSPORTATION,AMBTran,AMB,"Not VEHICLES, but public train stations, airports, bus stations, subway stations. Not the airplane or train itself. But public places centering around transportation.","Air, Airfield, Airport, Area, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Booth, Border, Bus, Cable, Car, Center, Concourse, Control, Crossing, Customs, Depot, Ferry, Freight, Garage, Heliport, Highway, Hub, Immigration, Inspection, Metro, Parking, Port, Rail, Rest, Seaport, Station, Stop, Subway, Terminal, Terminus, Toll, Tower, Traffic, Train, Tram, Transit, Transport, Truck, Tube, Vehicle"
|
||||||
|
AMBIENCE,TROPICAL,AMBTrop,AMB,"Jungles and rainforests, lush animal life and vegetation.","Amazon, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Biodiversity, Borneo, Bromeliad, Bush, Canopy, Caribbean, Cloud, Congo, Dengue, Equatorial, Exotic, Forest, Hawaiian, Humid, Island, Jungle, Lush, Mangrove, Mediterranean, Midlatitude, Monsoonal, Neotropical, Paradise, Rainforest, Semitropical, Southeast, Subhumid, Subtemperate, Subtropic, Subtropical, Subtropics, Sultry, Tropic, Tropical, Tropicalia, Tropicalian, Tropicalist, Tropics"
|
||||||
|
AMBIENCE,TUNDRA,AMBTndra,AMB,"Treeless environments where the ground is permanently frozen, arctic, antarctic.","Antarctic, Antarctica, Arctic, Arctics, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Badlands, Barren, Barrens, Dogsled, Dogsleds, Frozen, Glacier, Glaciers, Greenland, Ice, Icebergs, Icebound, Icecap, Icecaps, Icefield, Icefields, Icy, Igloos, Moss, Mosses, Muskeg, Muskegs, Muskox, North, Permafrost, Polar, Pole, Siberia, Snow, Snowless, South, Steppes, Subarctic, Treeless, Wastelands, Yukon"
|
||||||
|
AMBIENCE,UNDERGROUND,AMBUndr,AMB,"Subterranean, underground ambiences like sewers, bunkers, tunnels, dungeons, caves.","Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Basement, Belowground, Bomb, Bunker, Buried, Catacomb, Catacombs, Cave, Cavern, Caverns, Caves, Cellar, Crypt, Drips, Dungeon, Echoes, Groundwater, Hideout, Mine, Passage, Passageway, Sewer, Shaft, Shelter, Subsurface, Subterranean, Subterraneous, Subway, Sunken, Tunnel, Tunnels"
|
||||||
|
AMBIENCE,UNDERWATER,AMBUndwtr,AMB,"Underwater sounding ambiences, deep bubbles and that classic undersea world sound. See also WATER-UNDERWATER.","Aqualung, Aquanaut, Aquanauts, Aquarian, Aquarium, Aquatic, Aquatile, Atlantean, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Bathypelagic, Bathyscaphe, Bathysphere, Bottom, Cave, Chthonian, Chthonic, Coral, Deepsea, Deepwater, Diver, Divers, Frogmen, Kelp, Marine, Neptunian, Ocean, Oceanic, Reef, Sea, Seabed, Seafloor, Seamount, Seawater, Sharks, Shipwreck, Spearfishing, Spelunking, Subaquatic, Subareal, Subcontinental, Submerged, Submergible, Submerging, Submersibles, Submersion, Subsurface, Trench, Undersea, Underseas"
|
||||||
|
AMBIENCE,URBAN,AMBUrbn,AMB,"Dense cities and environments. New York City or London or Mumbai. Often Dense Traffic, horns, pedestrians, buses, but really any city. Overlap with AMBIENCE-TOWN possible.","Alley, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Bridge, Bustling, Center, Centre, Cities, Citified, City, Cityfied, Cityscape, Cityside, Civic, Civilian, Congestion, Cosmopolitan, Dense, District, Downtown, Financial, Ghetto, High, Honk, Hour, Inner, Innercity, Intercity, Interurban, Megacity, Metro, Metropolis, Metropolitan, Municipal, Municipalities, Overpass, Pedestrian, Plaza, Public, Rise, Row, Rush, Sidewalk, Skid, Skyscraper, Slum, Streetlife, Street, Streets, Traffic, Tunnel, Urbain, Urbana, Urbane, Urbanisation, Urbanism, Urbanist, Urbanistic, Urbanite, Urbanites, Urbanity, Urbanization, Urbanized, Urbanizing, Urbano, Urbanophilic, Urbans, Wash, Washy"
|
||||||
|
AMBIENCE,WARFARE,AMBWar,AMB,"Battles, warfare, full ambiences here. Not for isolated WEAPONS or VEHICLES which have their own categories. Composite sounds of war.","Air-Raid, Assault, Barracks, Atmos, Atmosphere, Ambience, Ambiance, BG, Background, Barrage, Base, Battle, Battlefield, Battles, Biowarfare, Blitz, Blitzkrieg, Blitzkriegs, Bombardment, Bombardments, Brigandage, Broadside, Bunker, Cannonade, Center, Checkpoint, Combat, Command, Conflict, Conquest, Engagement, Espionage, Fight, Flak, Foxhole, Fusillade, Hostilities, Invasions, Jihad, Jihads, Militarism, Military, Offensive, Operation, Operational, Operations, Raid, Salvo, Skirmish, Skirmishes, Skirmishing, Soldiering, Statecraft, Tactics, Terrorisation, Terrorism, Terrorization, Trench, Violence, War, Warfares, Warfighting, Warless, Warlike, Warring, Warriorship, Wartime, Weaponization, Weaponry, Zone"
|
||||||
|
ANIMALS,AMPHIBIAN,ANMLAmph,ANML,"Technically any amphibian, but most likely used for frogs and toads.","American, Amniotes, Amphibia, Axolotl, Axolotls, Bullfrog, Caecilian, Cane, Chorus, Croaker, Dart, Eastern, Fire-Bellied, Frog, Frogs, Leopard, Marbled, Mudfish, Mudpuppies, Newt, Northern, Peeper, Poison, Red-Eyed, Salamander, Salamanders, Spotted, Spring, Tadpole, Tadpoles, Terrapins, Toad, Toads, Tree, Western, Yellow-Bellied"
|
||||||
|
ANIMALS,AQUATIC,ANMLAqua,ANML,"All manner of marine and sea animals, not just mammals, but anything that lives in water.","Angelfish, Aquatic, Barracuda, Beluga, Cetacean, Clam, Clownfish, Crab, Crabs, Crustacean, Cuttlefish, Dolphin, Dugong, Eel, Eels, Elephant, Fish, Jellyfish, Killer, Lamprey, Lion, Lobster, Lobsters, Manatee, Manta, Marine, Narwhal, Ocean, Octopus, Orca, Otter, Porpoise, Ray, Scallop, Sea, Seahorse, Seal, Shark, Shrimp, Squid, Starfish, Stingray, Turtle, Urchin, Walrus, Whale"
|
||||||
|
ANIMALS,BAT,ANMLBat,ANML,"Bat chirps, echolocation and communicative vocalization. Wings would go under WINGS-MISC.","Bat, Belfry, Brown, Chiropteran, Echolocation, Flying, Fox, Fruit, Mammal, Nocturnal, Roosting, Roosts, Vampire, Winged"
|
||||||
|
ANIMALS,CAT DOMESTIC,ANMLCat,ANML,Domesticated house cats. See also ANIMALS-CAT WILD.,"Abyssinian, Alley, Burmese, Calico, Cat, Catfight, Coon, Egyptian, Fat, Felidae, Feline, Felines, Felis, Hairball, Himalayan, Hiss, Housecat, Kitten, Kittens, Kitties, Kitty, Maine, Meow, Persian, Purr, Pussycat, Siamese, Tabby, Tomcat, Tomcats, Whiskers"
|
||||||
|
ANIMALS,CAT WILD,ANMLWcat,ANML,Wild cats as opposed to domesticated cats. Tigers and Lions.,"Bengal, Big, Bobcat, Caracal, Cat, Cheetah, Cheetahs, Civet, Civets, Cougar, Cougars, Hiss, Jaguar, Leopard, Leopards, Lion, Lioness, Lionesses, Lions, Lynx, Lynxes, Manx, Ocelot, Ocelots, Panther, Polecats, Puma, Purr, Serval, Snow, Tiger, Tigers, Wildcat, Wildcats"
|
||||||
|
ANIMALS,DOG,ANMLDog,ANML,"All Domesticated dogs, excluding wild dogs. Pet dogs. See also ANIMALS-WILD DOG for non-domesticated dogs.",UCS marke
|
||||||
|
ANIMALS,DOG WILD,ANMLWdog,ANML,"Wild dogs and canines like Wolves, Foxes, Coyotes etc.","African, Arctic, Bush, Canid, Canids, Canines, Corsac, Coyote, Dhole, Dingo, Dingoes, Dog, Ethiopian, Fennec, Fox, Foxes, Gray, Hyaena, Hyena, Hyenas, Indian, Island, Jackal, Jackals, Lupus, Maned, Red, Wild, Wolf, Wolfish, Wolflike, Wolfs, Wolves"
|
||||||
|
ANIMALS,FARM,ANMLFarm,ANML,Domesticated mammals you'd find on a farm. For farm birds see BIRDS-FOWL. See also AMBIENCE-FARM.,"Alpaca, Angus, Barnyard, Beef, Bison, Bovine, Bovines, Buffaloes, Bullocks, Bulls, Cattle, Cattlemen, Cow, Domesticated, Domesticates, Donkey, Eew, Farms, Farmsteads, Farmyard, Feedlot, Goat, Goats, Hereford, Hogs, Holstein, Jersey, Lamb, Llama, Mule, Pig, Piggery, Piggies, Piglets, Ram, Reindeer, Ruminant, Ruminants, Sheep, Steer, Wagyu"
|
||||||
|
ANIMALS,HORSE,ANMLHors,ANML,"All members of the horse family, including donkeys and mules. See also FOOTSTEPS-HORSE and EQUIPMENT-BRIDLE.","Arabian, Arabians, Ass, Clydesdale, Colt, Donkey, Donkeys, Equestrian, Equestrians, Equid, Equids, Equine, Equines, Equus, Filly, Foal, Foals, Gelding, Geldings, Hoofs, Horse, Horseback, Horseflesh, Horseman, Horsemanship, Horsemen, Horseraces, Horsewoman, Horsey, Horsy, Jockeys, Mare, Mares, Mule, Mules, Mustang, Mustangs, Packhorses, Palominos, Ponies, Pony, Quarter, Racehorse, Racehorses, Riders, Saddles, Sawbuck, Shetland, Stables, Stallion, Stallions, Standardbreds, Steeds, Thoroughbred, Thoroughbreds, Zebra"
|
||||||
|
ANIMALS,INSECT,ANMLInsc,ANML,"All manner of traditional insect sounds, crickets, cicada, flies and bees. And singular insect recordings. See also AMBIENCE-INSECT now for dense ambience style recordings.","Ant, Aphid, Bee, Bees, Beetle, Beetles, Budgie, Bugs, Butterfly, Buzz, Caterpillar, Cicada, Cockroach, Conehead, Cricket, Damselfly, Dragonfly, Entomology, Firefly, Flea, Fly, Gnat, Grasshopper, Hornet, Katydid, Ladybug, Locust, Mantis, Monarch, Mosquito, Mosquitoes, Moth, Pests, Praying, Spider, Termite, Tick, Wasp, Weta"
|
||||||
|
ANIMALS,MISC,ANMLMisc,ANML,Other animals not finding a clear category.,"Miscellaneous, Zoology"
|
||||||
|
ANIMALS,PRIMATE,ANMLPrim,ANML,"All members of the primate family except humans. Apes, Chimps, etc.","Ape, Apes, Baboon, Baboons, Bonobo, Bonobos, Bushbaby, Capuchin, Chimpanzee, Chimpanzees, Chimps, Colobus, Gibbon, Gibbons, Gorilla, Gorillas, Great, Langur, Langurs, Lemur, Lemurs, Macaque, Macaques, Marmoset, Marmosets, Monkey, Monkeys, Orangutan, Orangutans, Primates, Prosimians, Squirrel, Tarsier, Tarsiers"
|
||||||
|
ANIMALS,REPTILE,ANMLRept,ANML,"All reptiles including snakes, turtles and lizzards.","Alligator, Alligators, Bearded, Boas, Caiman, Chameleon, Chameleons, Cobras, Crocodile, Crocodiles, Dragon, Dragons, Gecko, Geckos, Gharial, Gila, Iguana, Iguanas, Komodo, Lizard, Lizards, Monitor, Monitors, Monsters, Pythons, Rattlesnake, Reptiles, Serpents, Skink, Skinks, Snake, Snakes, Terrapin, Tortoise, Tortoises, Turtle, Turtles, Vipers"
|
||||||
|
ANIMALS,RODENT,ANMLRdnt,ANML,All manner 'gnawing' animals like rats and mice. Rodents.,"Beaver, Capybara, Chinchilla, Chipmunk, Chipmunks, Dog, Dormice, Gerbil, Gerbils, Gnawing, Gopher, Gophers, Groundhog, Guinea, Hamster, Hamsters, Jerboa, Jerboas, Kangaroo, Lemming, Marmot, Marmots, Mice, Mole, Mouse, Muskrat, Nutria, Opossums, Packrat, Pig, Porcupine, Possums, Prairie, Rat, Rats, Rodent, Rodents, Shrew, Squirrel, Vermin, Vole, Voles"
|
||||||
|
ANIMALS,WILD,ANMLWild,ANML,"Any wild animal not explicitly defined by the list, and not domesticated. Wolves and Coyotes now to go ANIMALS-DOG WILD","Aardvark, Animalism, Animalistic, Anteater, Antelope, Badger, Bear, Bison, Boar, Buffalo, Caribou, Carnivores, Creatures, Critters, Deer, Devil, Elephant, Elk, Endangered, Gazelle, Giraffe, Grisly, Hippo, Hippopotamus, Impala, Kangaroo, Koala, Mammals, Moose, Ox, Panda, Platypus, Polar, Predators, Predatory, Quadrupeds, Raccoon, Red, Reindeer, Rhino, Rhinoceros, Sloth, Tapir, Tasmanian, Wallaby, Warthog, Wildebeest, Wildness, Wolverine, Wombat"
|
||||||
|
ARCHIVED,ADR,ADR,ADR,Re-recorded lines via the ADR process.,"Additional, Automated, Dialog, Dubbing, Loop, Recording, Replacement, Voiceover"
|
||||||
|
ARCHIVED,ASSET,ASSET,ASSET,"A delivered or stored asset, most likely for video game use, but something used in the development of a project.","Asset, Game, Resource, Tag"
|
||||||
|
ARCHIVED,BOUNCE,BNCE,BNCE,"Bounces made for picture department, layered bounces.","Avid, Bounce, Crash, Crashdown, Down, Mix, Recordings, Temp"
|
||||||
|
ARCHIVED,IMPULSE RESPONSE,IR,IR,"Impulse Responses, reverb and otherwise.","Altiverb, Convolution, Early, Echo, Impulse, IR, Late, Reflection, Reflections, Response, Reverb, Sample, Tail"
|
||||||
|
ARCHIVED,LOOP GROUP,LPGRP,LPGRP,Loop group crowds or lines. Specifically recorded in a loop group envirment and needing to tracked as such.,"Actors, ADR, Group, Loop, Walla"
|
||||||
|
ARCHIVED,MIX,MIX,MIX,"Premixes, final mixes, mixdowns, stems.","5.1, 7.1, Atmos, Audio, Binaural, Blu-Ray, Dolby, DVD, Final, IMAX, M&E, Master, Mastering, Mix, Mixdown, Nearfield, Premix, Printmaster, Recordings, Remix, Stem, Stereo, Streaming, Surround, Theatrical"
|
||||||
|
ARCHIVED,PFX,PFX,PFX,FX Stolen from production takes only. Beware of possible rights issues.,"Effect, Production, Recording, Set"
|
||||||
|
ARCHIVED,PRODUCTION,PROD,PROD,"Production takes, dialog lines and other spoken line production takes.","Dialog, Dx, Line, Original, Recording, Set, Take"
|
||||||
|
ARCHIVED,RAW,RAW,RAW,"Raw recording files, for example a load of a full DAT tape.","DAT, Files, Original, Raw, Source, Tape, Unaltered, Unedited, Unmodified, Unprocessed, Untreated"
|
||||||
|
ARCHIVED,REFERENCE,REF,REF,"Reference material, often restricted, used for reference only. Beware of possible rights issues.","Contextual, Demo, Example, Explanatory, Guide, Identification, Informational, Inspiration, Instructional, Materials, Mock-Up, Placeholder, Refer, Referenced, References, Referencing, Researched, Researching, Resources, Sample, Temporary, Track"
|
||||||
|
ARCHIVED,SCENE,SCNE,SCNE,"Pre-built scenes and soundscapes, designed scenes. For example a fully build old-west scene.","Documentary, Episode, Footage, Montage, Movie, Prebuilt, Readymade, Scenario, Scene, Segment, Sequence, Series, TV"
|
||||||
|
ARCHIVED,TEST TONE,TEST,TEST,"Test tones used for calibration, ADR beeps, Pink Noise, Sine wave Sweeps.","2-Pops, ADR, Audio, Beeps, Bleeping, Calibration, Dolby, Frequency, Generator, IR, Monitoring, Noise, Pink, Reference, Signal, Sine, SMPTE, Square, Sweep, Sweeps, Test, Tones, Wave, White"
|
||||||
|
ARCHIVED,TRADEMARKED,TMARK,TMARK,"For trademarked or copyright sounds that you need to store in your library. Jingles, logos.","Brand, Branded, Branding, Brandname, Brandnames, Copyright, Copyrightable, Copyrighted, Corporate, Iconic, Infringed, Legal, Licensable, Licensed, Logo, Logos, Patent, Patented, Product, Proprietary, Protected, Registered, Restricted, Slogan, Sound, Trademark, Trademarks, Tradename, Tradenames"
|
||||||
|
ARCHIVED,WORK IN PROGRESS,WIP,WIP,"Files that aren't fully metadata'd, or mastered, tag to remind you there is work to do. Recordings awaiting mastering.","Backlog, In, Ongoing, Progress, Project, Task., Temp, Unfinished, Upcoming, WIP, Work, Workflow, Worklist, Worklog"
|
||||||
|
ARCHIVED,WTF,WTF,WTF,Literally what it says 'What the F_ck' is THIS sound... Strange otherwise uncategorizable sounds.,"Baffling, Jokes, Perplexing, Puzzling, Uncategorizable, Unknown"
|
||||||
|
BEEPS,APPLIANCE,BEEPAppl,BEEP,"Microwave oven beeps, dishwasher beeps.","Air, Blender, Bread, Cleaner, Coffee, Conditioner, Cooker, Cooktop, Dishwasher, Dishwashers, Dishwashing, Dryer, Electric, Fan, Food, Hair, Heater, Iron, Kettle, Machine, Maker, Microwave, Mixer, Oven, Processor, Refrigerator, Rice, Slow, Stand, Toaster, Vacuum, Washing"
|
||||||
|
BEEPS,GENERAL,BEEP,BEEP,Beep sounds not fitting another category in this list. User Interface beeps go to UI-BEEPS.,"Alarms, Alerts, Beep, Beeper, Bleep, Bleeper, Bleeps, Blinks, Blipping, Chimes, Chirps, Confirmation, Diagnostic, Error, Homing, Keypad, Menu, Navigation, Notifications, Pings, Shutdown, Signals, Tones, Warnings"
|
||||||
|
BEEPS,LOFI,BEEPLofi,BEEP,"Very low fidelity, old 8-bit beeps.","8-Bit, Analog, Atari, Classic, Colecovision, Distorted, Fuzzy, Nostalgic, Old-School, Retro, Static, Vintage"
|
||||||
|
BEEPS,MEDICAL,BEEPMed,BEEP,"Beeps from various medical devices, EKG, Life Support.","ECG, EEG, EKG, Flatline, Heart, KDC, Monitor, Oximeter, Pulse, Ultrasound"
|
||||||
|
BEEPS,TIMER,BEEPTimer,BEEP,"Specifically timer beeps, counting down. Classic time bomb beeps.","Countdown, Digital, Kitchen, Microwave, Oven, Phone, Timer, Watch"
|
||||||
|
BEEPS,VEHICLE,BEEPVeh,BEEP,Backup beeps from a large truck or construction vehicle.,"Alert, Backup, Beep, Blink, Blinker, Forklift, Reverse, Reversing, Signal, Warning"
|
||||||
|
BELLS,ANIMAL,BELLAnml,BELL,"Bells attached to animals, like cow and sheep bells, or a dog or cat collar with a bell on it.","Bell, Collar, Cowbell, Hawkbell, Sheep, Sleigh"
|
||||||
|
BELLS,DOORBELL,BELLDoor,BELL,Mechanical doorbells. Would also maybe electronic doorbells in here as well to avoid confusion probably. Also see ALARMS-BUZZERS for a door buzzer.,"Chime, Ding, Ding-Dong, Dong, Door, Doorbell, Ring"
|
||||||
|
BELLS,GONG,BELLGong,BELL,A large flat circular disc hit with a mallet. With v8.2 of the list most of these would now go into MUSICAL-BELLS instead.,"Agung, Bowl, Buddhist, Gamelan, Gong, Meditation, Tam-Tam, Temple, Tibetan"
|
||||||
|
BELLS,HANDBELL,BELLHand,BELL,"Small to medium hand bells, dinner bells. Musical bells like HANDBELLS and CARILLION now go into MUSICAL-BELLS.","Alter, Bells, Butler, Dinner, Hand, Jingle, School, Service"
|
||||||
|
BELLS,LARGE,BELLLrg,BELL,"Large bells like a church bell or tower bell, or city hall bells, Big Ben. Any large bell.","Ben, Big, Cathedral, Church, Liberty, Peal, Temple, Tower"
|
||||||
|
BELLS,MISC,BELLMisc,BELL,Bell sounds not fitting another category in this list.,"Bicycle, Fire, School, Train, Tram, Trolley"
|
||||||
|
BIRDS,BIRD OF PREY,BIRDPrey,BIRD,"Birds that prey on vertebrates, especially rodents. Raptors.","Avivore, Bald, Birdlover, Birdseye, Birdtrap, Buzzard, Caracara, Condor, Eagle, Eaglehawk, Falcon, Fishhawk, Golden, Goshawk, Harrier, Hawk, Hawkling, Kestrel, Kite, Merlin, Osprey, Owl, Peregrine, Raptor, Red-Tailed, Redtail, Sparhawk, Sparrowhawk, Swallow-Tailed, Talon, Verreaux, Vulture, White-Tailed"
|
||||||
|
BIRDS,CROW,BIRDCrow,BIRD,"Birds in Corvid family, which includes crows and ravens.","Beak, Blue, BluJay, Cawed, Cawing, Caws, Chough, Choughs, Corvid, Corvine, Crow, Crowe, Grackle, Jackdaw, Jay, Jays, Magpie, Nutcracker, Raven, Ravens, Rook, Rooks, Squawking, Treecreeper, Treepies"
|
||||||
|
BIRDS,FOWL,BIRDFowl,BIRD,"Landfowl, wildfowl and waterfowl, also poultry. Ground feeding and many water feeding birds.","Barnyard, Capon, Chachalacas, Chicken, Chitterling, Chukar, Cockerel, Curassow, Duck, Duckling, Ducks, Eider, Emu, Fowl, Fowle, Gamebirds, Geese, Giblet, Goose, Grebe, Grouse, Guans, Guineafowl, Hen, Hens, Mallard, Muscovy, Ostrich, Partridge, Partridges, Peafowl, Pheasant, Pheasants, Plumage, Poulterer, Poussin, Ptarmigan, Quail, Quails, Rooster, Squab, Swan, Teal, Turkey, Turkeys, Turkies, Waterfowl, Woodcocks"
|
||||||
|
BIRDS,MISC,BIRDMisc,BIRD,Bird sounds not fitting another category in this list.,"Ani, Cassowary, Cuckoo, Emu, Hummingbird, Kiwi, Nighthawk, Nightjar, Ostrich, Poorwill, Roadrunner, Woodpecker"
|
||||||
|
BIRDS,SEA,BIRDSea,BIRD,"Birds that live mostly near the ocean. Seagulls, pelicans.","Albatross, Auk, Auklet, Black-Legged, Booby, Bowerbird, Brown, Cormorant, Dunlin, Frigatebird, Fulmar, Gannet, Guillemot, Gull, Kittiwake, Migratory, Murrelet, Noddy, Oystercatcher, Pelican, Penguin, Petrel, Plover, Puffin, Razorbill, Sandpiper, Sandplover, Seabird, Seaduck, Seafowl, Seagull, Seahawk, Shearwater, Sheerwater, Shorebird, Skua, Skuas, Tern, Waterfowl, Widowbird, Yellow"
|
||||||
|
BIRDS,SONGBIRD,BIRDSong,BIRD,"Specifically singing birds, warblers, robins, thrushes, what you imagine pretty dawn chorus birds.","Antechinus, Bellbird, Bellbirds, Birdcall, Birdy, Blackbird, Bluebird, Bobwhite, Bowerbird, Bowerbirds, Budgerigar, Bullfinch, Bunting, Canary, Cardinal, Chaffinch, Chat, Chickadee, Chirrups, Cisticola, Cockatiel, Creeper, Dipper, Dove, Finch, Flycatcher, Gnatcatcher, Greenfinch, Grosbeak, Hawfinch, Honeyeater, Kinglet, Lark, Lovebird, Meadowlark, Mockingbird, Nightingale, Nuthatch, Oriole, Oscine, Ovenbird, Parula, Penduline, Pigeon, Robin, Serin, Skylark, Songful, Songster, Songstress, Sparrow, Starling, Swallow, Tanager, Thrasher, Thrush, Tit, Titmouse, Towhee, Treecreeper, Troupial, Vireo, Warble, Warbler, Warblers, Warbles, Waxwing, Whitethroat, Woodlark, Wren"
|
||||||
|
BIRDS,TROPICAL,BIRDTrop,BIRD,"Birds living in tropical locations, jungle and tropical rainforest birds. Parrots and Toucans.","Antbird, Aracari, Barbet, Bellbird, Bulbul, Cockatoo, Coquette, Eye, Firebird, Honeycreeper, Hornbill, Ibis, Jacamar, Kookaburra, Lorikeet, Lyrebird, Macaw, Manakin, Mango, Motmot, Mynah, Mynas, Parakeet, Parrot, Parroting, Parrots, Peacock, Potoo, Puffbird, Quetzal, Runner, Shoebill, Skimmer, Sunbird, Tanager, Toucan, Trogon, Tropicbird"
|
||||||
|
BIRDS,WADING,BIRDWade,BIRD,"Long-legged, long-billed birds that feed primarily in shallow water. Think Herons and Egrets.","Avocet, Bittern, Blue, Bower, Coot, Crane, Curlew, Dowitcher, Egret, Flamingo, Godwit, Heron, Ibis, Little, Oystercatcher, Plover, Rail, Redshank, Ruff, Sanderling, Sandpiper, Shorebird, Snipe, Sora, Spoonbill, Stork, Wader, Waterbird, Waterfowl, Whimbrel, Wildfowl, Yellowlegs"
|
||||||
|
BOATS,AIR BOAT,BOATAir,BOAT,Specifically large fan airboats used in swamps. Think the type of boat used in the Everglades. Also hovercraft.,"Air, Amphibious, Boat, Fan, Hovercraft, Swamp"
|
||||||
|
BOATS,BOW WASH,BOATWash,BOAT,"The waves pushed up by a boat or ship going through the water, a relatively clean recording.","Bow, Propeller, Ripple, Wake, Wave"
|
||||||
|
BOATS,DOOR,BOATDoor,BOAT,Boat or Submarine doors. Some possible overlap with over DOOR categories or VEHICLES-DOOR.,"Access, Boat, Bulkhead, Cabin, Door, Ferry, Gangway, Gate, Hatch, Ship, Trapdoor, Watertight"
|
||||||
|
BOATS,ELECTRIC,BOATElec,BOAT,"Any boat powered by electricity. Fairly rare. Possible a a radio controlled boat, or electric trolling motor.","DC, Electrical, Motor, Powered, Trolling"
|
||||||
|
BOATS,FISHING,BOATFish,BOAT,"Industrial fishing boats, crab boats, trawlers. Simple fishing boats to BOATS-MOTORBOAT.","Angling, Bass, Boat, Crab, Dredger, Fisher, Fisheries, Fisherman, Fishermen, Fishery, Fleet, Gillnetter, Jon, Line, Research, Seiner, Sportfishing, Trawl, Trawler, Trawlers, Vessel, Vessels"
|
||||||
|
BOATS,HORN,BOATHorn,BOAT,Horns on boats and ships. Fog horns would go in HORNS-AIR category.,"Air, Blast, Canal, Departure, Ferry, Horn, Ports, Ship, Tugboat, Warning"
|
||||||
|
BOATS,INTERIOR,BOATInt,BOAT,"Interior ambiences on boats and ships, ferries, cruise ships, navy ships.","Aboard, Balcony, Belowdecks, Berth, Bilge, Boat, Bowels, Bunk, Cabin, Cargo, Cockpit, Cruise, Engine, Ferries, Ferry, Freighter, Galley, Head, Helm, Inside, Interior, Military, Navy, Room, Sailboat, Salon, Ship, Stateroom, Stowaway, Submarine, Tanker, Tugboat, Yacht"
|
||||||
|
BOATS,MECHANISM,BOATMech,BOAT,"All manner of boat mechanisms. Sailing ship rigging, pulleys, ropes, ship wheels. See also BOATS-DOOR.","Anchor, Cage, Davit, Grapnel, Jigger, Net, Propeller, Pulley, Rigging, Rope, Rudder, Ship, Throttle, Trap, Wheel, Winch, Windlass"
|
||||||
|
BOATS,MILITARY,BOATMil,BOAT,"Specifically military boats and ships, destroyers, aircraft carriers. Any military boat.","Aircraft, Amphibious, Armada, Attack, Battlecruiser, Battleship, Blockade, Boat, Carrier, Corvette, Craft, Cruiser, Cruisier, Destroyer, Dreadnought, Fast, Fleet, Flotilla, Frigate, Gunboat, Landing, Naval, Patrol, Submarine, Torpedo, Warship"
|
||||||
|
BOATS,MISC,BOATMisc,BOAT,Boat sounds not fitting another category in this list.,"Afloat, Barge, Boatswain, Bosun, Capsize, Capsizing, Paddleboard, Paddleboat"
|
||||||
|
BOATS,MOTORBOAT,BOATMotr,BOAT,"General small boats, personal fishing boats, speedboats, pontoon boats. You average recreational boat.","Bass, Boat, Bowrider, Cabin, Cruiser, Duck, Fishing, Personal, Pontoon, Powerboat, Runabout, Ski, Small, Speedboat"
|
||||||
|
BOATS,RACING,BOATRace,BOAT,"Powered racing boats, not for human powered racing boats, those would go under BOATS-ROWING.","Boat, Cigarette, Drag, Hydrofoil, Hydroplane, Jet, Powerboat, Regatta, Ski"
|
||||||
|
BOATS,ROWBOAT,BOATRow,BOAT,"Canoes, rowboats, kayaks probably, human powered boats that use oars and paddles, even racing rowboats.","Canoe, Canoes, Cutter, Dinghy, Dory, Gondola, Inflatable, Jon, Kayak, Lifeboat, Longboat, Oar, Outrigger, Paddle, Punt, Raft, Row, Rowboat, Scow, Scull, Skiff, Wherry"
|
||||||
|
BOATS,SAILBOAT,BOATSail,BOAT,"Composite recordings of rigging and sails, isolated elements to CLOTH-FLAP for close sails, or BOATS-MECHANISM for rigging.","Barkentine, Barque, Barquentine, Brig, Brigantine, Catamaran, Centerboard, Clipper, Cutter, Dhow, Dinghy, Junk, Keelboat, Ketch, Longboat, Monohull, Pinnace, Proa, Regatta, Sailboat, Schooner, Shallop, Sloop, Trimaran, Yacht, Yawl"
|
||||||
|
BOATS,SHIP,BOATShip,BOAT,"Large ship, passenger cruise ships, oil tankers, things relating to these.","Cargo, Container, Cruise, Ferry, Freighter, Icebreaker, Large, Oil, Passenger, Pirate, Research, Ship, Tanker, Tugboat, Vessel"
|
||||||
|
BOATS,STEAM,BOATStm,BOAT,"Old steam paddle boats, big steam ship sounds.","Boiler, Engine, Paddlewheel, Riverboat, Ship, Steamboat, Steamer, Steamship, Sternwheeler, Titanic"
|
||||||
|
BOATS,SUBMARINE,BOATSub,BOAT,"Relating to a submarine, include sonar pings. Classic 'Dive' alarm would go in ALARMS-SIREN with keyword Klaxon.","AUV, Deep-Sea, Nuclear-Powered, Research, ROV, Sonar, Sub, Submarine, Submersible, Subs, Torpedo, Torpedoing, U-Boat"
|
||||||
|
BOATS,UNDERWATER,BOATUndwtr,BOAT,"Specifically boats and ships recorded underwater, usually hydrophone recordings. SUBMARINES have their own subcategory.","Recorded, Submerged, Undersea"
|
||||||
|
BULLETS,BY,BLLTBy,BLLT,"Bullet pass bys in air, or underwater.","Bullet, Fwip, Graze, Subsonic, Supersonic, Whiz-By, Whizby"
|
||||||
|
BULLETS,IMPACT,BLLTImpt,BLLT,"Bullet hits and impacts, on a bullet hit on any material from the ground, to wood, to a person.","Armor, Ballistic, Body, Bullet, Bulletproof, Gunshot, Headshot, Hit, Impact, Killshot, Penetration, Pierce, Shot, Strike, Struck, Vest, Wounded"
|
||||||
|
BULLETS,MISC,BLLTMisc,BLLT,Bullet sounds not fitting another category in this list.,"Air, Ammunitions, Armor, Bandoliers, BB, Birdshot, Blanks, Buckshot, Bullet, Cartridges, Dummy, Frangible, Incendiary, Miscellaneous, Paintball, Pellet, Piercing, Reloads, Rifle, Rounds, Rubber, Slug, Subsonic, Supersonic, Tracer"
|
||||||
|
BULLETS,RICOCHET,BLLTRico,BLLT,"Bullet ricochets, that classic old west 'ping' when a bullet bounces off something.","Deflect, Ricochet, Ricos, Whizzing"
|
||||||
|
BULLETS,SHELL,BLLTShel,BLLT,Bullet shells ejecting and dropping to the ground. See also GUNS-HANDLING for things like loading or unloading a gun.,"Ammo, Ammunition, Cartridge, Casing, Eject, Housing, Shell"
|
||||||
|
CARTOON,ANIMAL,TOONAnml,TOON,"Cartoon animals, made from animals calls, props, etc. Very silly or stylized animals calls for use in Cartoons.","Animal, Animated, Call, Cartoony, Comic, Duck, Fake, Hanna, Hunting, Silly, Toon"
|
||||||
|
CARTOON,BOING,TOONBoing,TOON,"Cartoon boings, like for a bouncing cartoon character. Roger Rabbit or Tigger from Winnie the Pooh.","Animated, Cartoony, Comic, Hanna, Harp, Jaw, Mouth, Pogo, Silly, Spring, Stick, Toon"
|
||||||
|
CARTOON,CLANG,TOONClang,TOON,"Cartoon clangs, like a funny anvil hit. Most likely a very metallic resonant hit.","Animated, Anvil, Bong, Cartoony, Clink, Comic, Hanna, Metallic, Silly, Toon"
|
||||||
|
CARTOON,CREAK,TOONCreak,TOON,"Cartoon creaks, some overlap or confusion with CARTOON-STRETCH possible.","Animated, Cartoony, Comic, Hanna, Screech, Silly, Toon, Wood"
|
||||||
|
CARTOON,HORN,TOONHorn,TOON,"Cartoon horns, as in bulb horns, clown horns.","Animated, Bugle, Bulb, Cartoony, Clown, Comic, Hanna, Honk, Silly, Toon, Trumpet"
|
||||||
|
CARTOON,IMPACT,TOONImpt,TOON,"Cartoon hits, bonk, boink, doink..","Animated, Bang, Biff, Boff, Boink, Bonk, Cartoony, Clobber, Comic, Doink, Hanna, Hit, Punch, Shtoink, Silly, Smack, Strike, Thunk, Toon, Wham, Zonk"
|
||||||
|
CARTOON,MACHINE,TOONMach,TOON,"Cartoon machines, an overly comical sounding machine or contraption. A Rube Goldberg contraption.","ACME, Animated, Apparatus, Cartoony, Comic, Comical, Contraption, Device, Gadget, Gizmo, Goldberg, Hanna, Machine, Rube, Silly, Toon, Trap"
|
||||||
|
CARTOON,MISC,TOONMisc,TOON,Cartoon sounds not fitting another category in this list.,"Animated, Blorb, Cartoony, Comic, Flump, Hanna, Silly, Squanch, Toon"
|
||||||
|
CARTOON,MUSICAL,TOONMx,TOON,Cartoon musical phrases. String plucks for a cartoon character sneaking up on another. Perhaps a cartoon musical phrase.,"Animated, Ascend, Cartoony, Comic, Descend, Gliss, Hanna, Melodic, Musical, Silly, Toon"
|
||||||
|
CARTOON,PLUCK,TOONPluk,TOON,"Cartoon string plucks, violin plucks, dental floss plucks.","Animated, Cartoony, Comic, Hanna, Plink, Pluck, Silly, String, Toon"
|
||||||
|
CARTOON,POP,TOONPop,TOON,"Cartoon funny pops, mouth pops, suction cup pops.","Animated, Bubble, Bubblegum, Cartoony, Comic, Cup, Gun, Hanna, Mouth, Pop, Pops, Silly, Suction, Toon"
|
||||||
|
CARTOON,SHAKE,TOONShake,TOON,"Cartoon shake, like a noisemaker toy, maraca.","Animated, Cartoony, Comic, Hanna, Rattle, Silly, Toon, Tremble"
|
||||||
|
CARTOON,SPLAT,TOONSplt,TOON,"Cartoon splats, funny liquid and mud like splats and things.","Animated, Cartoony, Comic, Hanna, Silly, Splat, Splort, Squelch, Squish, Toon"
|
||||||
|
CARTOON,SQUEAK,TOONSqk,TOON,Cartoon squeaks. funny little sounding squeaks of things.,"Animated, Cartoony, Comic, Hanna, Rubber, Silly, Squeak, Toon"
|
||||||
|
CARTOON,STRETCH,TOONStrch,TOON,"Cartoon stretches, funny rubber or other type stretching sounds, exaggerated.","Animated, Cartoony, Comic, Elongate, Extend, Hanna, Lengthen, Pull, Silly, Strain, Toon, Yank"
|
||||||
|
CARTOON,SWISH,TOONSwsh,TOON,"Cartoon swishes, specifically very funny cartoony swishes.","Animated, Cartoony, Comic, Hanna, Silly, Swirl, Swoosh, Toon, Twirl, Whoosh"
|
||||||
|
CARTOON,TWANG,TOONTwang,TOON,"Cartoon twangs, that classic ruler twang type sound.","Animated, Band, Cartoony, Comic, Hanna, Harp, Jaw, Pluck, Rubber, Ruler, Silly, String, Toon, Twanging"
|
||||||
|
CARTOON,VEHICLE,TOONVeh,TOON,"Cartoon vehicles, think Jetsons spaceship or Flintstones cars.","Animated, Backfire, Cartoony, Comic, Contraption., Flintstones, Hanna, Jetsons, Silly, Toon"
|
||||||
|
CARTOON,VOCAL,TOONVox,TOON,"Cartoon vocals, specifically very silly. See also CARTOON-ANIMALS for non-human vocals.","Animated, Cartoony, Comic, Grumble, Hanna, Mumble, Mutter, Ramble, Silly, Toon, Voice"
|
||||||
|
CARTOON,WARBLE,TOONWarb,TOON,Cartoon warble.,"Animated, Cartoony, Comic, Hanna, Quaver, Silly, Toon, Vibrato, Warbling, Wobble"
|
||||||
|
CARTOON,WHISTLE,TOONWhis,TOON,"Cartoon whistles, most likely slide whistles, other very silly whistles. A Wolf-Whistle.","Animated, Blow, Cartoony, Comic, Hanna, Silly, Slide, Toon, Toot, Whistle, Wolf"
|
||||||
|
CARTOON,ZIP,TOONZip,TOON,"Cartoon zips. Very fast 'zip' type sounds, often for fast movement aways.","Animated, Cartoony, Comic, Dart, Flash, Fly, Hanna, Rico, Silly, Toon, Whizz, Zing, Zip, Zippy, Zoom"
|
||||||
|
CERAMICS,BREAK,CERMBrk,CERM,"Ceramics break of a more singular nature, a plate shatter, breaking, cracking, crunching. Mostly for single items breaking. See also CERAMICS-CRASH & DEBRIS.","Apart, Break, Burst, Ceramic, Ceramics, China, Chip, Clay, Crack, Crockery, Crumble, Crunch, Crush, Demolish, Destroy, Disintegrate, Earthenware, Fracture, Fragment, Porcelain, Pottery, Shatter, Smash, Snap, Splinter, Split, Stoneware, Terracotta, Tile, Ware"
|
||||||
|
CERAMICS,CRASH & DEBRIS,CERMCrsh,CERM,"Ceramics destruction of more of a group nature, multiple crashes and debris. See also CERAMICS-BREAK.","Ceramic, Ceramics, China, Clay, Collision, Crockery, Debris, Earthenware, Fall, Fragments, Porcelain, Pottery, Remains, Rubble, Ruins, Scatter, Shards, Smash, Stoneware, Terracotta, Tile, Trash, Ware, Wreckage"
|
||||||
|
CERAMICS,FRICTION,CERMFric,CERM,"Ceramics friction, scraping, squeaking, screeching.","Abrasion, Attrition, Ceramic, Ceramics, China, Clay, Creak, Crockery, Earthenware, Erosion, Grating, Grinding, Porcelain, Pottery, Rasping, Rubbing, Scouring, Scrape, Scraping, Scratching, Screech, Scuffing, Sliding, Squeak, Stoneware, Stress, Terracotta, Tile, Ware, Wear"
|
||||||
|
CERAMICS,HANDLE,CERMHndl,CERM,"Ceramics handling, picking up a coffee mug, handling a plant pot. Some overlap with CERAMICS-MOVEMENT.","Caress, Catch, Ceramic, Ceramics, China, Clasp, Clay, Clench, Clutch, Crockery, Down, Earthenware, Embrace, Fondle, Grab, Grasp, Grip, Handle, Hold, Manipulate, Operate, Palm, Pickup, Porcelain, Pottery, Seize, Set, Stoneware, Take, Terracotta, Tile, Toss, Touch, Use, Ware"
|
||||||
|
CERAMICS,IMPACT,CERMImpt,CERM,"Ceramics impacts without breaking, so hits drops, clunks and clinks. See also CERAMICS-BREAK.","Bang, Banging, Bash, Bump, Ceramic, Ceramics, China, Clap, Clay, Clink, Clunk, Collide, Colliding, Collision, Crash, Crashing, Crockery, Drop, Earthenware, Hit, Hitting, Impact, Impacting, Jolt, Knock, Porcelain, Pottery, Pound, Ram, Shock, Slam, Slamming, Smack, Smacking, Stoneware, Strike, Striking, Terracotta, Thrust, Thump, Tile, Ware"
|
||||||
|
CERAMICS,MISC,CERMMisc,CERM,Ceramics sounds not fitting another category in this list.,"Ceramic, Ceramics, China, Clay, Crockery, Earthenware, Miscellaneous, Porcelain, Pottery, Stoneware, Terracotta, Tile, Ware"
|
||||||
|
CERAMICS,MOVEMENT,CERMMvmt,CERM,"Ceramics single or group movement, so dragging, shaking and rattling. See also CERAMICS-HANDLE.","Ceramic, Ceramics, China, Clatter, Clay, Crockery, Drag, Earthenware, Jiggle, Jingle, Move, Porcelain, Pottery, Rattle, Roll, Shake, Shift, Stoneware, Terracotta, Tile, Vibrate, Ware, Wobble"
|
||||||
|
CERAMICS,TONAL,CERMTonl,CERM,"Ceramics ringing, as in being bowed.","Bowed, Ceramic, Ceramics, China, Clay, Crockery, Earthenware, Frequency, Harmonic, Melodic, Melodious, Musical, Ping, Pitch, Porcelain, Pottery, Resonance, Resonant, Ring, Shing, Sonorous, Sound, Stoneware, Terracotta, Tile, Timbre, Tonal, Tone, Ware"
|
||||||
|
CHAINS,BREAK,CHAINBrk,CHAIN,"Chain snapping or breaking, that sharp sound of a chain failing.","Bend, Break, Burst, Crack, Fracture, Links, Rupture, Sever, Shatter, Snap, Splinter, Split"
|
||||||
|
CHAINS,HANDLE,CHAINHndl,CHAIN,"Chain handling, repairing a bike chain, a prisoner moving around in chains. Some overlap with CHAINS-MOVEMENT.","Catch, Clench, Down, Grab, Grasp, Grip, Handle, Hold, Manipulate, Operate, Palm, Pickup, Pulling, Seize, Set, Take, Throw, Toss, Use"
|
||||||
|
CHAINS,IMPACT,CHAINImpt,CHAIN,"Chain impacts, such a big chain hit or slam, or a tension snap of a chain being pulled taut.","Bang, Banging, Colliding, Crash, Crashing, Drop, Hit, Hitting, Impact, Impacting, Pound, Ram, Slam, Slamming, Smack, Smacking, Strike, Striking, Thrust"
|
||||||
|
CHAINS,MISC,CHAINMisc,CHAIN,Chain sounds not fitting another category in this list.,"Bonds, Links, Manacles, Miscellaneous, Restraints, Shackles"
|
||||||
|
CHAINS,MOVEMENT,CHAINMvmt,CHAIN,"Chain handling, particularly being pulled through pulleys or across surfaces. See also CHAINS-HANDLE.","Bind, Bound, Clank, Clatter, Drag, Jingle, Shackle, Shackled, Shake, Vibrate"
|
||||||
|
CHEMICALS,ACID,CHEMAcid,CHEM,"Chemical acid burning and sizzling, bubbling. A vat of acid.","Acetic, Acidic, Acrid, Biting, Caustic, Citric, Corrosive, Erosive, Fizz, Hydrochloric, Melt, Sizzle, Sour, Sulfuric, Toxic"
|
||||||
|
CHEMICALS,MISC,CHEMMisc,CHEM,Chemical sounds not fitting another category in this list.,"Agents, Atoms, Chemical, Chemicals, Compound, Compounds, Formulas, Matter, Miscellaneous, Mixtures, Molecules, Reagent, Solutions, Substance, Substances, Toxic"
|
||||||
|
CHEMICALS,REACTION,CHEMReac,CHEM,"Chemical reactions, chemistry set, bubbling, foaming, bunsen burner reactions. Overlap perhaps with LIQUID & MUD category.","Activate, Bubbling, Catalysis, Catalyst, Catalyze, Chemically, Chemistry, Electrolysis, Endothermic, Enzyme, Exothermic, Fermentation, Foaming, Hydrolysis, Inactive, Inert, React, Reaction, Reactive, Reagent, Transformation"
|
||||||
|
CLOCKS,CHIME,CLOCKChim,CLOCK,"Chimes, bells specifically for a clock, Grandfather clock chimes. Alarm clocks would go to ALARMS-CLOCK.","Bell, Chime, Clock, Dong, Grandfather, Peal, Ring, Sound, Strike, Striker, Toll, Tolling"
|
||||||
|
CLOCKS,MECHANICS,CLOCKMech,CLOCK,"Clocks winding, complex mechanical clock mechanisms.","Apparatus, Clocklike, Clockwork, Clockworks, Cogwheel, Cuckoo, Device, Escapement, Gears, Innards, Insides, Machinery, Mainspring, Mechanisms, Movements, Pendulum, Springs, Ticktock, Timepiece, Watch, Wheels, Winder, Winding, Workings"
|
||||||
|
CLOCKS,MISC,CLOCKMisc,CLOCK,Clock sounds not fitting another category in this list. Also see ALARMS-CLOCK,"Chronometers, Miscellaneous, Timekeepers, Timepieces, Timers, Watches"
|
||||||
|
CLOCKS,TICK,CLOCKTick,CLOCK,"Analog ticking clocks and watches of all sizes, also mechanical stop watches.","Chronometer, Click, Clicking, Clock, Egg, Face, Hands, Numerals, Pendulum, Quartz, Sounds, Stop, Stopwatch, Tick, Tick-Tock, Ticking, Time, Timekeeping, Timer, Tock, Tocking, Watch"
|
||||||
|
CLOTH,FLAP,CLOTHFlp,CLOTH,"Cloth flapping, as in a flag flapping, or superman's cape. Also ship sails and parachutes.","Apparel, Apron, Banner, Bib, Blanket, Burlap, Canvas, Cape, Clothe, Cotton, Dress, Fabric, Fabrics, Flag, Flannel, Flap, Flapping, Flicker, Flutter, Fluttering, Garment, Gown, Jacket, Jeans, Khaki, Lace, Laundry, Linen, Material, Microfiber, Muslin, Nylon, Oilcloth, Oilskin, Pants, Parachute, Pillowcase, Polyester, Rag, Rayon, Robe, Rustle, Rustling, Sail, Sailcloth, Sheet, Shirt, Silk, Skirt, Tablecloth, Terry, Textile, Towel, Whipping, Wool, Woven"
|
||||||
|
CLOTH,HANDLE,CLOTHHndl,CLOTH,"Cloth handling, grabbing a jacket, clothy arm grabs, etc. Some overlap with CLOTH-MOVEMENT. See also possibly OBJECTS-FASHION.","Apparel, Apron, Banner, Bib, Blanket, Burlap, Canvas, Catch, Clasp, Clench, Clothe, Clutch, Cotton, Down, Dress, Embrace, Fabric, Fabrics, Flag, Flannel, Garment, Gown, Grab, Grasp, Grip, Handle, Hold, Jacket, Jeans, Khaki, Lace, Laundry, Linen, Material, Microfiber, Muslin, Nylon, Oilcloth, Oilskin, Pants, Pickup, Pillowcase, Polyester, Rag, Rayon, Robe, Seize, Set, Sheet, Shirt, Silk, Skirt, Tablecloth, Terry, Textile, Toss, Towel, Wool, Woven"
|
||||||
|
CLOTH,IMPACT,CLOTHImpt,CLOTH,"Cloth impacts, throwing a jacket forcefully to the floor, a wet towel snap. Some overlap with CLOTH-MOVEMENT possible.","Apparel, Apron, Banner, Bib, Blanket, Bump, Burlap, Canvas, Clothe, Cotton, Dress, Drop, Fabric, Fabrics, Flag, Flannel, Garment, Gown, Hit, Hitting, Impact, Impacting, Jacket, Jeans, Khaki, Lace, Laundry, Linen, Material, Microfiber, Muslin, Nylon, Oilcloth, Oilskin, Pants, Pillowcase, Polyester, Rag, Rayon, Robe, Sheet, Shirt, Silk, Skirt, Smack, Smacking, Strike, Tablecloth, Terry, Textile, Thump, Towel, Wool, Woven"
|
||||||
|
CLOTH,MISC,CLOTHMisc,CLOTH,Cloth sounds not fitting another category in this list.,"Apparel, Apron, Banner, Bib, Blanket, Burlap, Canvas, Chambray, Cloth, Clothe, Corduroy, Cotton, Denim, Dress, Fabric, Fabrics, Flag, Flannel, Garment, Gown, Jacket, Jeans, Khaki, Lace, Laundry, Linen, Material, Microfiber, Miscellaneous, Muslin, Nylon, Oilcloth, Oilskin, Pants, Pillowcase, Polyester, Rag, Rayon, Robe, Sheet, Shirt, Silk, Skirt, Tablecloth, Terry, Textile, Towel, Twill, Velvet, Wool, Woven"
|
||||||
|
CLOTH,MOVEMENT,CLOTHMvmt,CLOTH,Varied cloth movement except flapping which has its own category. Variety of cloth movement. Cloth zuzz. See also CLOTH-FLAP.,"Adjusting, Apparel, Apron, Arranging, Banner, Bib, Billow, Billowing, Blanket, Bunching, Burlap, Canvas, Clothe, Cotton, Crumpling, Drape, Draping, Dress, Fabric, Fabrics, Flag, Flannel, Flapping, Flow, Flutter, Fluttering, Folding, Garment, Gathering, Gown, Hang, Jacket, Jeans, Khaki, Lace, Laundry, Linen, Material, Microfiber, Movement, Muslin, Nylon, Oilcloth, Oilskin, Pants, Pillowcase, Pleating, Polyester, Positioning, Rag, Rayon, Repositioning, Ripple, Robe, Ruffle, Ruffling, Rustle, Rustling, Scrunch, Scrunching, Settling, Sheet, Shifting, Shirt, Silk, Skirt, Smoothing, Spreading, Stretching, Sway, Swaying, Swing, Swish, Tablecloth, Terry, Textile, Towel, Tucking, Tugging, Undulate, Unfolding, Waving, Wool, Woven, Zuzz"
|
||||||
|
CLOTH,RIP,CLOTHRip,CLOTH,"Various cloth rips and tears, short to long. Velcro would best fit here too.","Apart, Apparel, Apron, Banner, Bib, Blanket, Breach, Burlap, Canvas, Clothe, Cotton, Cut, Dissect, Dress, Fabric, Fabrics, Flag, Flannel, Garment, Gash, Gown, Incise, Jacket, Jeans, Khaki, Lace, Lacerate, Laundry, Linen, Material, Microfiber, Muslin, Nylon, Oilcloth, Oilskin, Pants, Perforate, Pillowcase, Polyester, Puncture, Rag, Rayon, Rend, Rip, Ripped, Ripper, Ripping, Rive, Robe, Sever, Sheet, Shirt, Shred, Shredded, Silk, Skirt, Slash, Sunder, Tablecloth, Tattered, Tatters, Tear, Tearing, Terry, Textile, Towel, Velcro, Wool, Woven"
|
||||||
|
COMMUNICATIONS,AUDIO VISUAL,COMAv,COM,"Various AV devices such a projector or tape recorder, video cameras here too. Some overlap or confusion with COMMUNICATIONS-CAMERA.","16mm, 8mm, Amplification, Amplifier, Audio, Audiovisual, Aural, AV, Betamax, Bias, Bolex, Cables, Camcorder, Camera, Cassette, CD, Cinematography, Conferencing, Device, Digital, Dollies, Film, Filming, Footage, Hypermedia, Ipod, Laserdisc, Media, Mixer, Monitoring, Motion, Multimedia, Nagra, Picture, Playback, Player, Presentation, Projector, Radiotelephone, Receiver, Recorder, Reel, Render, Slide, Sound, Spooling, Streaming, System, Tape, Taping, Telecommunication, Television, To, Transient, Tripod, VCR, Video, Videocam, Videophone, Visual, Walkman, Wall"
|
||||||
|
COMMUNICATIONS,CAMERA,COMCam,COM,"Still camera shutters, automatic winding, old flash-bulbs. Movie and video cameras to COMMUNICATIONS-AV.","120mm, 35mm, Action, Autofocus, Body, Bulb, Cam, Camara, Camera, Cameraman, Cameramen, Cameraperson, Camra, Canon, CCTV, Digicam, Digital, DSLR, Film, Flash, Format, Fuji, Fujifilm, Hasselblad, Image, Imaging, Instant, Kodak, Leica, Lens, Lense, Mamiya, Medium, Minicam, Nikon, Olympus, Photo, Photog, Photograph, Photographer, Photographic, Photographing, Photography, Picture, Pictures, Point-And-Shoot, Polaroid, Sensor, Shutter, SLR, Sony, Surveillance, Trail, Viewfinder, Webcam, Winding"
|
||||||
|
COMMUNICATIONS,CELLPHONE,COMCell,COM,"Cellphones and Smartphones., clicks and rings, tones.","Android, Blackberry, Calling, Cameraphone, Carphone, Cell, Cellular, Femtocell, Flip, Galaxy, Handsfree, Iphone, Mobile, Motorola, Nokia, Phone, Pocketphone, Portability, Roaming, Screenphone, SIM, Smartphone, Wireless, Wristphone"
|
||||||
|
COMMUNICATIONS,MICROPHONE,COMMic,COM,Feedback and mic bumps and handling. Megaphones would go here too.,"Cardioids, Dictaphone, Electret, Feedback, Handling, Headsets, Hydrophone, Hypercardioid, Lapel, Larsen, Lavalier, Lavaliere, Lectern, Megaphone, Mic, Microphone, Mics, Mike, Mikes, Podium, Preamplifier, Tap, Transducer"
|
||||||
|
COMMUNICATIONS,MISC,COMMisc,COM,Communication sounds not fitting another category in this list.,"Comm, Communicator, Fax, Miscellaneous, Newsletter, Reporting, Telecom, Telecommunication, Telecommunications, Telecoms, Telematics, Transmittal, Transmitted"
|
||||||
|
COMMUNICATIONS,PHONOGRAPH,COMPhono,COM,"Turntable, Victrola, motors and mechanics. STATIC has its own subcategory here.","33, 45, 78, Audion, Changer, Cinematograph, Edison, EP, Gramophone, Gramophones, Graphogram, Graphophone, Jukebox, Jukeboxes, Kinetoscope, LP, Orchestrion, Phonautograph, Phono, Phonogram, Phonographic, Phonography, Phonorecord, Photophone, Pianola, Player, Portable, Radiogram, Record, Stereophonic, Stereopticon, Stylus, Turntable, Turntables, Victrola, Vinyl"
|
||||||
|
COMMUNICATIONS,RADIO,COMRadio,COM,"Tuning, radio and shortwave sounds. STATIC has its own subcategory here. A radio broadcast would go here too. Overlap with VOICES-FUTZED.","Airing, Airwaves, AM, Antenna, BBC, Boombox, Broadcast, Broadcaster, Broadcasting, Car, Clock, Communications, FM, Futz, HD, Longwave, Newscasting, Portable, Radar, Radial, Radiocast, Radiocommunication, Radiofrequency, Radiophonic, Radiotelegraph, Radiotelegraphy, Radiotelephone, Receiver, Receiving, Shortwave, Side-Band, Signal, Siriusxm, Squelch, Station, Tabletop, Transmission, Tuner, Tuning, Waves, Weather, Wireless"
|
||||||
|
COMMUNICATIONS,STATIC,COMStatic,COM,"Static from any electronic source, radio, CB, television, optical noise, phonograph noise.","Aliasing, Brown, Buzz, Crackle, Crunchy, Electrostatic, Fuzz, Hiss, Hum, Interference, Noise, Noisy, Pink, Radio, Scramble, Squelched, TV, White"
|
||||||
|
COMMUNICATIONS,TELEMETRY,COMTelm,COM,"Fax machine tones, modem tones, telegraph, teletype. UI is it's own category","Analog, Analytics, Code, Comms, Data, Datalink, Datalogger, Dataloggers, Digital, Downlink, Downlinks, FSK, GPS, Modem, Monitoring, Morse, PPM, PSK, Ranging, Readout, Readouts, Satellites, Sensors, SOS, Spectrum, Telegraph, Tracker, Trackers, Tracking, Transmission, Uplinking"
|
||||||
|
COMMUNICATIONS,TELEPHONE,COMTelph,COM,Old school rotary dial and digital telephones. Technically any telephone that isn't a mobile or cell phone.,"Answerphone, Autodial, Calling, Cord, Cordless, Dial, Dialer, Dialing, Directory, Handset, Helpdesk, Helpline, Hotline, Hotlines, Interphone, Key, Landline, Number, Numero, Pad, Payphone, Phone, Phonecall, Phoned, Phoneline, Phoning, Push-Button, Radiotelephone, Radiotelephony, Receiver, Rotary, Speakerphone, Switchboard, Switchboards, Tel, Tele, Telecom, Telecommunication, Telecommunications, Teleconference, Teleconferencing, Telefonica, Telemarketing, Telephonic, Telephonically, Telephonist, Telephonists, Telephony, Touch-Tone, Touchtone, Voicemail"
|
||||||
|
COMMUNICATIONS,TELEVISION,COMTv,COM,"Television programs, on and off, channel changes, TV Remote controls. STATIC has its own subcategory here. A television broadcast would go here too. Overlap with VOICES-FUTZED.","4K, Advertising, Antenna, Broadcast, Broadcaster, Channel, Commercials, CRT, Curved, Dial, Documentaries, Drama, Emissions, Flatscreen, Futz, HDR, LCD, LED, Movies, News, Newscaster, OLED, Opera, Plasma, Program, Programming, QLED, Remote, Show, Sitcom, Sitcoms, Smart, Soap, Sports, Talk, Teevee, Teleshopping, Televised, Television, Telly, Tube, TV, UFH, VHF, Viewing"
|
||||||
|
COMMUNICATIONS,TRANSCEIVER,COMTran,COM,"Walkie Talkies, CBs, Ham Radios, all two-way transceivers and radios go here. Put voices into VOICES-FUTZED.","Allonge, Antenna, Array, Bleep, Broadcast, CB, Collect, Collecting, Compander, Converter, Cradle, Cross, Directional, Dispatch, Downlink, Ear, Frequency, Ham, Heterodyne, Intercom, Keying, Lead, Modulation, Multipath, Omnidirectional, Racon, Radar, Radio, Radiogram, Radioman, Radios, Radiosonde, Receive, Received, Receiver, Receiving, Rig, Satellite, Signal, Squelch, Talkies, Transmitter, Transponder, Two-Way, Walkie, Wavelength, Wireless"
|
||||||
|
COMMUNICATIONS,TYPEWRITER,COMType,COM,"Old school typewriters, not modern computer keyboards.","Braillewriter, Brother, Carriage, Corona, Daisywheel, Electric, Keyboard, Knob, Manual, Mimeographs, Olivetti, Olympia, Platen, Platten, Portable, Return, Ribbon, Royal, Smith-Corona, Stenograph, Stenographer, Tabulator, Teleprinter, Teletype, Teletypes, Teletypewriters, Treadle, Typeball, Typebar, Typebars, Typeface, Typehead, Typewrite, Typewriter, Typewritten, Typist, Typists, Typograph, Typographer, Underwood"
|
||||||
|
COMPUTERS,HARD DRIVE,CMPTDriv,CMPT,"Laptop or other hard drive seeking sounds, data reading.","Boot, Data, Disc, Disk, Firewire, Floppy, Gigabytes, Grind, Hard, HDD, IDE, Magnetic, Megabytes, NAS, RAID, SATA, SCSI, Search, SSD, Storage, Terabytes, Thunderbolt"
|
||||||
|
COMPUTERS,KEYBOARD & MOUSE,CMPTKey,CMPT,"Typing on computer keyboard, and mouse movement.","Bluetooth, Clicking, Cursor, Device, Ergonomic, Input, Keyboard, Keypad, Keys, Keystroke, Magic, Mechanical, Mice, Mouse, Numeric, Optical, QWERTY, Touchpad, Trackball, Trackpad, Typing, Wired, Wireless"
|
||||||
|
COMPUTERS,MISC,CMPTMisc,CMPT,Computer sounds not fitting another category in this list. Printers would go to MACHINES-OFFICE.,"Calculation, Computing, CPU, Cybercrime, Cyberspace, Desktop, Desktops, Gadget, Laptop, Mainframe, Palm, Pc, Peripheral, Pilot, Pocket, Software, Supercomputer, Tablet, Tech"
|
||||||
|
CREATURES,AQUATIC,CREAAqua,CREA,"Giant mutant squids or sea monsters, kraken, leviathans.","Aquatic, Creature, Cthulhu, Giant, Hydra, Kelpie, Kraken, Leviathan, Loch, Marine, Mermaid, Monster, Naga, Ness, Nessie, Oceanic, Sea, Serpent, Siren, Squid"
|
||||||
|
CREATURES,AVIAN,CREAAvian,CREA,"Alien or Fantasy birds, or weird birdlike creatures. A giant mutant parakeet would go here.","Alien, Beaked, Birds, Extraterrestrial, Fantasy, Feathered, Flying, Garuda, Griffin, Harpy, Hippogriff, Monster, Phoenix, Roc, Simurgh, Thunderbird, Winged"
|
||||||
|
CREATURES,BEAST,CREABeast,CREA,"Big four legged creatures and beasts. Wooly Mammoth, a mutant elephant, Mumakil.","Abomination, Beast, Cerberus, Chupacabra, Creature, Cryptid, Mammoth, Minotaur, Monster, Mumakil, Quadruped, Sasquatch, Unicorn, Wooly, Yeti"
|
||||||
|
CREATURES,BLOB,CREABlob,CREA,"Gelatinous and gooey type creatures, gelatinous cube.","Amorphous, Blob, Blobby, Formless, Gelatinous, Gloop, Gooey, Goop, Jelly-Like, Mass, Monster, Ooze, Shapeless, Slime, Slimy, Viscous"
|
||||||
|
CREATURES,DINOSAUR,CREADino,CREA,"Dinosaurs of all types, Jurassic Park.","Allosaur, Allosaurus, Ankylosaurus, Anteosaur, Apatosaurus, Archaeopteryx, Archosaur, Argentinosaur, Beastie, Brachiosaurus, Brontosaur, Brontosaurs, Brontosaurus, Carnosaur, Cerapodan, Ceratopsian, Deinosaur, Dino, Dinocarid, Dinoceratan, Dinos, Dinosauriform, Dinosauromorph, Dinosaurus, Dinotherium, Diplodocus, Duckbilled, Exhibition, Fossil, Fossilised, Fossilized, Fossils, Hadrosaur, Hadrosaurs, Hung, Ichthyosaur, Ichthyosaurus, Iguanodon, Jurassic, Mesosaur, Mesosaurid, Mesozoic, Mosasaur, Non, Nothosaur, Ornithischian, Ornithopod, Oviraptor, Palaeontologist, Palaeontology, Palaeosaur, Paleontologist, Paleontology, Parasaurolophus, Pelagosaur, Plateosaur, Plesiosaur, Prehistoric, Prosauropod, Protorosaur, Pteranodon, Pterodactyl, Pterodactyls, Pterosaur, Pterosaurs, Raptor, Reptile, Rex, Rhynchosaur, Saurian, Saurians, Saurischian, Sauropod, Sauropods, Saurs, Segnosaurid, Spinosaur, Spinosaurid, Spinosaurus, Stegosaur, Stegosaurs, Stegosaurus, Teleosaur, Therizinosaurid, Theropod, Titanosaur, Triceratops, Trilophosaurid, Tyrannosaur, Tyrannosaurid, Tyrannosaurs, Tyrannosaurus, Velociraptor"
|
||||||
|
CREATURES,DRAGON,CREADrgn,CREA,"Game of Thrones, Lord of the Rings, dragons of all types. See also CREATURES-REPTILIAN.","Chimera, Draco, Drago, Dragon, Dragoness, Dragonlike, Dragonslayer, Drake, Firedrake, Hydra, Naga, Serpent, Wyrm, Wyrmling, Wyvern, Wyverns"
|
||||||
|
CREATURES,ELEMENTAL,CREAElem,CREA,"Creatures based on elements, rock giants, fire creatures, ice monsters, such a the Balrog, or a Rock Giant.","Air, Alchemic, Alchemical, Arcane, Crystal, Earth, Fire, Giant, Golem, Ice, Monster, Primal, Primeval, Rock, Shadow, Snow, Water, Wood"
|
||||||
|
CREATURES,ETHEREAL,CREAEthr,CREA,"Ghosts, spirits, ethereal vocals and wispy movement, apparitions.","Aethereal, Angel, Apparition, Apparitional, Astral, Banshee, Being, Corporeal, Ephemeral, Ethereal, Floaty, Ghost, Ghostlike, Ghosts, Gossamer, Otherworldly, Phantasmal, Phantasmic, Phantom, Poltergeist, Soul, Specter, Spectral, Spirit, Spirits, Vaporous, Vapory, Wisp, Wispy, Wraith"
|
||||||
|
CREATURES,HUMANOID,CREAHmn,CREA,"Human sized mythical creatures, so zombies and vampires, elves etc. Troll, Ogres, Orcs. Two legged creatures.","Bigfoot, Dead, Elf, Frankenstein, Ghoul, Living, Mummy, Ogre, Orcs, Swamp, Tengu, Thing, Troll, Undead, Vampire, Werewolf, Witch, Zombie"
|
||||||
|
CREATURES,INSECTOID,CREAInsc,CREA,Giant spiders or other insect like creatures. The creature from The Fly. A giant Tarantula.,"Ant, Arachnid, Giant, Spider, Wasp"
|
||||||
|
CREATURES,MISC,CREAMisc,CREA,Creature sounds not fitting another category in this list.,"Cryptids, Folklore, Miscellaneous, Mythical, Mythological"
|
||||||
|
CREATURES,MONSTER,CREAMnstr,CREA,"Godzilla, scary monsters, horror monsters. See also CREATURES-HUMANOID.","Behemoth, Chupacabra, Godzilla, King, Kong, Rancor, Xenomorph, Yeti"
|
||||||
|
CREATURES,REPTILIAN,CREARept,CREA,"Giant lizards and snakes, large giant hissing monsters.","Basilisk, Giant, Gorgon, Lizard, Medusa, Serpent, Serpentine, Snake"
|
||||||
|
CREATURES,SMALL,CREASmall,CREA,"Specifically small mythical creatures. Fairies, pixies, small rodent like creatures, etc. Distinguished by small size.","Brownie, Cupid, Elf, Elves, Fairy, Fraggle, Gnome, Gremlin, Hobgoblin, Imp, Leprechaun, Nymph, Pixie, Sprite"
|
||||||
|
CREATURES,SOURCE,CREASrce,CREA,"Source recordings meant for creature design, like prop recordings, human voice packs for design, etc.","Barks, Bellows, Breath, Call, Caw, Chatters, Chitter, Click, Cries, Cry, Effort, Groans, Growls, Grunt, Grunts, Gurgles, Guttural, Hiss, Hisses, Howls, Moans, Mouth, Roars, Scream, Screams, Screech, Screeches, Shrieks, Snarls, Squawk, Squeal, Wails, Whine, Whispers, Yowls"
|
||||||
|
CROWDS,ANGRY,CRWDAngr,CRWD,"Crowds of upset or angry people, as in a riot, protest, a schoolyard fight. See also AMBIENCE-PROTEST.","Agitated, Audience, Congregation, Demonstrators, Enraged, Furious, Gathering, Hooligan, Horde, Hostile, Incensed, Infuriated, Looters, Mad, Mob, Multitude, Outraged, Protest, Protesters, Riot, Rioters, Rowdy, Shout, Throng, Violent, Wrathful"
|
||||||
|
CROWDS,APPLAUSE,CRWDApls,CRWD,Crowds where the applause is predominantly featured. Use CROWDS-CHEERING for recordings that are mainly vocals.,"Acclaimed, Adulation, Applaud, Applauded, Applauding, Applause, Appreciated, Appreciating, Appreciative, Audience, Cheer, Cheering, Cheers, Clap, Clapping, Congratulation, Congregation, Encore, Enthusiastic, Gathering, Golf, Grandstand, Grandstander, Grandstanding, Hand, Hand-Clapping, Handclap, Horde, Multitude, Ovation, Praise, Slow, Throng"
|
||||||
|
CROWDS,BATTLE,CRWDBatl,CRWD,"Crowds in battle, fighting, yelling screaming, dying. If including gunfire, explosions. use AMBIENCE-WARFARE.","Army, Battalion, Battle, Chants, Charge, Charging, Combat, Conflict, Cries, Dying, Fighting, Horde, Infantry, Massacre, Melee, Multitude, Rally, Screaming, Scuffle, Shootout, Shout, Shouts, Showdown, Throng, Troop, Trooping, Troops, War, Yells"
|
||||||
|
CROWDS,CELEBRATION,CRWDCele,CRWD,"Crowds celebrating holidays, parties, rowdy crowds. If not clean, use AMBIENCE-CELEBRATION.","Audience, Awards, Birthday, Celebrate, Celebrating, Celebratory, Christmas, Election, Excited, Festive, Holiday, Jubilant, New, Parade, Party, Revel, Revelers, Rowdy, Victory, Years"
|
||||||
|
CROWDS,CHEERING,CRWDCheer,CRWD,"Could include applause, but cheering crowds where the crowd vocals are predominant. Overlap with CROWDS-CELEBRATION.","Audience, Awards, Celebratory, Chants, Cheerful, Cheering, Concert, Elated, Euphoric, Excited, Exultant, Graduation, Happy, Joyful, Jubilant, Parade, Political, Rally, Rejoicing, Roaring, Rousing, Sports, Stadium, Swells, Victory, Whooping"
|
||||||
|
CROWDS,CHILDREN,CRWDChld,CRWD,"Relatively clean vocals of children together, playing in a playground, in a school classroom, recess, school hallway. See also AMBIENCE-SCHOOL.","Babies, Children, Daycare, Elementary, Highschool, Infants, Juveniles, Kids, Kindergarten, Little, Minors, Ones, Park, Play, Playground, Playgroup, Playing, Recess, School, Schoolyard, Tag, Toddlers, Tots, Youngsters, Youths"
|
||||||
|
CROWDS,CONVERSATION,CRWDConv,CRWD,"A small enough group chat that you can discern words, smaller and less active than a walla. A chatter.","Casual, Chat, Chatter, Chitchat, Colloquy, Communication, Confabulation, Conversation, Conversations, Converse, Dialogue, Discussion, Exchange, Informal, Parley, Small, Socializing, Sparse, Talk, Verbalization, Walla"
|
||||||
|
CROWDS,LAUGHTER,CRWDLaff,CRWD,"Crowds laughing, a comedy show, canned television laughter, groups of kids laughing, at a party for example.","Amusement, Cackles, Canned, Chortles, Chuckles, Club, Comedy, Funny, Giggles, Guffaws, Ha-Ha, Hilarity, Jest, Joke, Laugh, Mirth, Snickers, Track, Uproar"
|
||||||
|
CROWDS,LOOP GROUP,CRWDLoop,CRWD,"For loop group type walla, usually performed for or to picture, specific environments, specific callouts. See also ARCHIVED-LOOP GROUP for official loop group recordings.","Actor, Actors, ADR, Canned, Cast, Conversations, Dubbing, Extras, Group, Indistinct, Loop, Performers, Walla"
|
||||||
|
CROWDS,MISC,CRWDMisc,CRWD,Crowd sounds not fitting another category in this list. See also MOVEMENT-CROWD.,Miscellaneous
|
||||||
|
CROWDS,PANIC,CRWDPanic,CRWD,"Distressed crowds, mixed screams and crying and reactions… crowds in a natural disaster or crowds in a giant monster attack...","Aggressive, Anxiety, Chaos, Cry, Crying, Desperate, Disaster, Disorder, Disorders, Distress, Emergencies, Fear, Fearful, Fleeing, Frantic, Hysteria, Hysterical, Mayhem, Pandemonium, Panic, Panicked, Pleas, Pushing, Rioting, Rout, Scrambling, Scream, Screaming, Shout, Shouting, Terror, Terrorists, Trapped, Victim, Yelling"
|
||||||
|
CROWDS,QUIET,CRWDQuiet,CRWD,"Very quiet crowds, mostly mumbling or murmuring. Like crowds whispering in church or a courtroom scene. See also VOICES-WHISPER for single person recordings.","Calm, Church, Conversations, Courtroom, Gallery, Hushed, Hushing, Library, Low, Mumbling, Murmur, Murmurs, Museum, Muted, Noiseless, Peaceful, Serene, Silent, Soft, Soundless, Still, Subdued, Talking, Tranquil, Voices, Whispering"
|
||||||
|
CROWDS,REACTION,CRWDReac,CRWD,"Big Ooos, and Aaahs and Booos, big group responses to events happening. Shocked reactions.","Aahs, Ahh, Applause, Boo, Booing, Boos, Chanting, Cheers, Excited, Gasps, Hollers, Hoots, Laughter, Murmurs, Ooh, Oohs, Reaction, Response, Shocked, Sighs, Studio, Whistling, Woohoo"
|
||||||
|
CROWDS,SINGING,CRWDSing,CRWD,"Singing during sporting events, New Year's Eve crowds singing Auld Lang Syne, Christmas carols. Not large choral music, which would go in MUSICAL category.","Acapella, Barbershop, Carol, Chamber, Chant, Chanted, Chanting, Choir, Chorale, Chorus, Club, Ensemble, Glee, Gospel, Harmony, Quartet, Recital, Vocal, Vocals"
|
||||||
|
CROWDS,SPORT,CRWDSprt,CRWD,"Clean crowds at sporting events, Baseball, football. For mixed ambience used AMBIENCE-SPORT. A sports chant.","Anthems, Applause, Athletics, Audience, Baseball, Basketball, Blowing, Booing, Chanting, Cheer, Cheering, Clapping, Contest, Drumming, Event, Events, Fans, Football, Games, Goal, Hockey, Horn, Insults, Match, Matches, Meets, Olympics, Race, Score, Shouting, Soccer, Spectator, Sporting, Stadium, Stomping, Taunts, Tournament, Victory, Whistling"
|
||||||
|
CROWDS,WALLA,CRWDWalla,CRWD,"Specifically very clean crowd walla, with non-discernible words. See also various AMBIENCE categories.","Background, Clamor, Clatter, Commotion, Conversation, Conversations, Din, Group, Hubbub, Indistinct, Murmur, Talk, Voices, Walla"
|
||||||
|
DESIGNED,BASS DIVE,DSGNBass,DSGN,Deep descending bass notes. Bass Drops.,"Bass, Descending, Dive, Downer, Drop, Fall, Low-Frequency, Pitch, Rumble, Sub-Bass, Subsonic, Trailer, Vibration"
|
||||||
|
DESIGNED,BOOM,DSGNBoom,DSGN,"Deep short and tight, low end stingers, big trailer deep hits. LFE type low end sweeteners.","Bang, Blast, Boom, Bump, Burst, Clap, Crash, Deep, Hit, Hypersonic, Impact, Low, Shockwave, Sting, Supersonic, Thump, Thunderous, Trailer"
|
||||||
|
DESIGNED,BRAAM,DSGNBram,DSGN,"Long deep horn like sounds. Like a War of the World's type long sound, or like a long french horn braam type sound.",Bramm
|
||||||
|
DESIGNED,DISTORTION,DSGNDist,DSGN,Particulary distored designed sounds and distortion elements.,"Buzz, Crackle, Distorted, Distortion, Feedback, Fuzz, Hiss, Hum, Noise, Off, Overdrive, Squared, Static"
|
||||||
|
DESIGNED,DRONE,DSGNDron,DSGN,"Designed drones, eerie tense drones, long stylized sounds with not much activity or movement.","Ambient, Drone, Droning, Hum, Humming, Monotonous, Ominous, Pad, Pulsing, Resonant, Sustained, Tension, Texture, Throb, Throbbing, Whirring"
|
||||||
|
DESIGNED,EERIE,DSGNErie,DSGN,"Often designed, but tense strange undefined sounds that are eerie, tense, suspenseful. Some overlap with DESIGNED-DRONE possible.","Apprehension, Bleak, Chilling, Creepy, Disturbing, Eerie, Freaky, Frightful, Ghostly, Grim, Haunting, Horror, Mysterious, Ominous, Otherworldly, Queer, Scary, Shadowed, Sinister, Spooky, Strange, Supernatural, Tense, Tension, Uncanny, Unnatural, Unsettling, Weird"
|
||||||
|
DESIGNED,ETHEREAL,DSGNEthr,DSGN,"Ethereal wispy designed sounds. Ghostlike whispers, or heavenly tones. Otherworldly. See also DESIGNED-EERIE.","Aethereal, Airy, Angelic, Celestial, Divine, Dreamy, Ethereal, Ghostly, Gossamer, Heavenly, Immaterial, Otherworldly, Spectral, Spirit, Surreal, Transcendent, Unearthly, Vaporous, Wispy"
|
||||||
|
DESIGNED,GRANULAR,DSGNGran,DSGN,"Granular synthesis, sounds that have a gritty textural type sound, think PaulStretch, sounds broken down into tiny slices and recompiled.","Grained, Grains, Grainy, Granular, Granulated, Gritty, Particles, Paulstretch, Slices, Textured"
|
||||||
|
DESIGNED,IMPACT,DSGNImpt,DSGN,"Designed huge impacts, collisions, stylized big hits. Possible overlap with DESIGNED-STINGER.","Blow, Bump, Collision, Crash, Hit, Impact, Jolt, Knock, Slam, Smash, Strike, Thud, Trailer"
|
||||||
|
DESIGNED,MISC,DSGNMisc,DSGN,Designed sounds not fitting another category in this list.,Miscellaneous
|
||||||
|
DESIGNED,MORPH,DSGNMorph,DSGN,"A stylized blending of elements, something transitioning into another thing.","Alter, Blend, Change, Convert, Evolving, Metamorphosis, Morph, Mutate, Reshape, Shape, Transfiguration, Transform, Transformation, Transition, Transmutation, Transmute"
|
||||||
|
DESIGNED,RISER,DSGNRise,DSGN,"Tension risers, things rising in tone, something similar to a rising string line to create tension.","Ascend, Ascending, Climbing, Crescendo, Escalate, Frequency, Intensify, Pitch, Reverse, Riser, Rising, Rizer, Soar, Surge, Tension, Tone, Trailer"
|
||||||
|
DESIGNED,RUMBLE,DSGNRmbl,DSGN,Designed rumbles for layering into a mix. Earthquake rumbles. Composite rumble.,"Bass, Deep, Earthquake, Grumble, Quake, Rumble, Shake, Subharmonic, Subsonic, Subwoofer, Tremble, Vibrate"
|
||||||
|
DESIGNED,RHYTHMIC,DSGNRythm,DSGN,"Stattaco or glitchy machine type loops, things with definied starts, stops, rhythmic stutters.","Beat, Glitch, Pattern, Pulse, Rhythmic, Staccato, Stutter, Temp"
|
||||||
|
DESIGNED,SOURCE,DSGNSrce,DSGN,"For recordings meant to be used as source for other designed sounds, experimental recordings, elements, etc.","Material, Raw, Recording, Source"
|
||||||
|
DESIGNED,STINGER,DSGNStngr,DSGN,"Stings, stylized screen sweeteners. Big trailer type sounds. Big shock scare type sounds. Stylized stings, hits and impacts. Overlap with DESIGNED-IMPACT.","Cinematic, Piercing, Slam, Stab, Startle, Sting, Stinging, Trailer"
|
||||||
|
DESIGNED,SYNTHETIC,DSGNSynth,DSGN,"Very synthesized sounds, very electronic synth stuff. Raw work files of synth-play.","Analog, Digital, Granular, Mod, Modular, Synth, Synthesized"
|
||||||
|
DESIGNED,TONAL,DSGNTonl,DSGN,Designed sounds with a particularly tonal nature. The ringing in your ear of tinnitus for example.,"Bowed, Chord, Frequency, Harmonic, Harmonious, Melodic, Melodious, Musical, Note, Ping, Pitch, Resonance, Resonant, Ring, Ringing, Shell, Shing, Shock, Sonorous, Sound, Subjective, Timbre, Tinnitus, Tone, Vibration"
|
||||||
|
DESIGNED,VOCAL,DSGNVocl,DSGN,"Design specifically around vocals, ghostly whispers, enchanted spells, reversed talking etc. Not for Creatures.","Bellow, Enchantment, Ghostly, Howl, Oral, Processed, Roar, Scream, Screech, Spell, Verbal, Vocal, Vocalized, Voiced, Wail, Whisper"
|
||||||
|
DESIGNED,WHOOSH,DSGNWhsh,DSGN,"Stylized designed big whooshes, definite possible overlap confusion with SHOOSHES-WHOOSH, but this for less natural and more stylized versions.","Air, By, Dopplered, Fast, Flying, Motion, Movement, Rapid, Rushing, Speeding, Swish, Swishes, Swooping, Swoosh, Swooshes, Trailer, Whirr, Whooshed, Whooshes, Whooshing"
|
||||||
|
DESTRUCTION,COLLAPSE,DESTRClpse,DESTR,"Buildings and large mixed complex collapses, a barn collapse, Godzilla pushing over a building.","Break, Cave, Collapse, Crumble, Crumple, Demolish, Demolition, Disintegrate, Down, Failure, Fall, Fell, Implode, In, Sinkhole, Structure, Topple"
|
||||||
|
DESTRUCTION,CRASH & DEBRIS,DESTRCrsh,DESTR,"Mixed crashes, complex crashes involving more than one material. So a combo huge glass and metal crash. See also individual material categories like METAL, WOOD etc.","Bender, Break, Car, Crash, Debris, Destroy, Detritus, Fender, Fragments, Implode, Implosion, Pileup, Plane, Rubble, Ruin, Rupture, Shatter, Shipwreck, Smash, Splinter, Train, Wreck, Wreckage"
|
||||||
|
DESTRUCTION,MISC,DESTRMisc,DESTR,Destruction sounds not fitting another category in this list.,Miscellaneous
|
||||||
|
DIRT & SAND,CRASH & DEBRIS,DIRTCrsh,DIRT,"Dirt or sand settling, as in after an explosion, big sprays of dirt and sand. DUST has its own subcategory.","Airborne, Ashes, Cinder, Cinders, Clay, Cloud, Crumble, Crumbs, Debris, Dirt, Disperse, Dust, Dusty, Earth, Earthy, Gravel, Grime, Grit, Gritty, Particulates, Pebbles, Plume, Pollen, Powdered, Quartz, Sand, Sandstorm, Sawdust, Sediment, Silt, Soil, Soot, Spray, Trickle"
|
||||||
|
DIRT & SAND,DUST,DIRTDust,DIRT,"Settling dust, dust clouds, much lighter sound than DIRT & SAND-DEBRIS","Airborne, Ashes, Cinder, Cinders, Cloud, Crumbs, Debris, Dirt, Disperse, Dust, Dusty, Earth, Earthy, Fine, Flecks, Fragments, Granules, Grime, Grit, Gritty, Motes, Particle, Particles, Particulate, Particulates, Plume, Pollen, Powder, Powdered, Sand, Sandstorm, Sawdust, Sediment, Silt, Small, Soil, Soot, Specks, Tiny"
|
||||||
|
DIRT & SAND,HANDLE,DIRTHndl,DIRT,"Handling dirt & sand, hands digging through dirt, children making a sand castle. Some overlap with DIRT & SAND-MOVEMENT.","Airborne, Ashes, Catch, Cinder, Cinders, Clench, Crumbs, Dig, Disperse, Earth, Form, Grab, Grasp, Grime, Grip, Grit, Handle, Hold, Manipulate, Mold, Particulates, Pollen, Powdered, Sawdust, Sculpt, Shape, Silt, Soil, Soot, Throw, Toss, Touch, Work"
|
||||||
|
DIRT & SAND,IMPACT,DIRTImpt,DIRT,"Big hits and impacts of dirt of sand, dirt clods. Think the huge dirt ball impacts in WHERE THE WILD THINGS ARE.","Airborne, Ashes, Bang, Bump, Cinder, Cinders, Clod, Clump, Crumbs, Disperse, Drop, Dump, Dusty, Earth, Grime, Grit, Heap, Hit, Knock, Lump, Particulates, Pollen, Powdered, Sawdust, Silt, Slam, Soil, Soot, Thud, Thump, Unload"
|
||||||
|
DIRT & SAND,MISC,DIRTMisc,DIRT,"Dirt, Sand and Earth sounds not fitting another category in this list.","Airborne, Ashes, Cinder, Cinders, Crumbs, Disperse, Dusty, Earth, Earthy, Grime, Grit, Gritty, Miscellaneous, Particulates, Pollen, Powdered, Sawdust, Silt, Soil, Soot"
|
||||||
|
DIRT & SAND,MOVEMENT,DIRTMvmt,DIRT,"Pouring, flowing of dirt or sand, a big sand pour. See also DIRT & SAND-HANDLE.","Airborne, Ashes, Cascade, Cinder, Cinders, Crumbs, Digging, Disperse, Displacement, Drift, Earth, Flow, Grime, Grit, Move, Particulates, Piling, Pollen, Pour, Pouring, Powdered, Sawdust, Shift, Shifting, Silt, Slide, Soil, Soot, Spill, Swirl, Unload, Upheaval"
|
||||||
|
DIRT & SAND,TONAL,DIRTTonl,DIRT,Singing sand dunes. Probably pretty rare.,"Airborne, Ashes, Cinder, Cinders, Crumbs, Disperse, Dunes, Earth, Frequency, Grime, Grit, Harmonic, Melodic, Melodious, Musical, Particulates, Pitch, Pollen, Powdered, Resonance, Resonant, Sand, Sands, Sawdust, Silt, Singing, Soil, Sonorous, Soot, Sound, Timbre, Tonal, Tone"
|
||||||
|
DOORS,ANTIQUE,DOORAntq,DOOR,"Very old sounding doors, as in an old castle or abbey. But massive doors of this type probably to DOOR-DUNGEON.","Abbey, Aged, Ancient, Antiquated, Antique, Barn, Cabin, Castle, Church, Classic, Cottage, Distressed, Door, Farmhouse, Historic, Historical, Old, Old-Fashioned, Period, Restored, Retro, Traditional, Victorian, Vintage, Weathered"
|
||||||
|
DOORS,APPLIANCE,DOORAppl,DOOR,"Appliance doors such as oven, dishwasher, refrigerator or microwave.","Dishwasher, Dryer, Freezer, Fridge, Laundry, Machine, Microwave, Oven, Refrigerator, Washing"
|
||||||
|
DOORS,CABINET,DOORCab,DOOR,"Cabinet doors, such as kitchen cabinet, tool cabinets.","Armoire, Bar, Bathroom, Buffet, Cabinet, Center, China, Cupboard, Display, Door, Entertainment, Hutch, Kitchen, Linen, Locker, Medicine, Pantry, Storage, Tool, Vanity, Wardrobe, Wine"
|
||||||
|
DOORS,COMPOSITE,DOORComp,DOOR,"Fiberglass, wood steel combination, doors that don't sound like a specific material. A modern front door. Some confusion or overlap with DOORS-WOOD likely.",Fiberglass
|
||||||
|
DOORS,CREAK,DOORCreak,DOOR,"Door creaks, wood or metal, overlap with WOOD-FRICTION and METAL-FRICTION, but specifically door creaks.","Castle, Cellar, Creak, Creaky, Dungeon, Groan, Haunted, Metal, Old, Rusty, Scary, Screech, Squeak, Squeaky, Wood"
|
||||||
|
DOORS,DUNGEON,DOORDungn,DOOR,"That big old classic wood and/or metal heavy dungeon, castle type door.","Castle, Cellar, Chamber, Crypt, Fortress, Large, Medieval, Oubliette, Passage, Secret, Stronghold, Torture, Tower"
|
||||||
|
DOORS,ELECTRIC,DOORElec,DOOR,"Electric doors of all kinds, garage doors, big soundstage doors, sliding store doors. See also DOORS-REVOLVING.","Automatic, Door, Electric, Garage, Hangar, Motorized, Power, Soundstage, Warehouse"
|
||||||
|
DOORS,GATE,DOORGate,DOOR,"Non-motorized gates, metal or wood. So a garden gate, a park gate. A farm gate.","Barricade, Barrier, Cattle, Crossing, Farm, Fence, Garden, Gate, Park, Rail"
|
||||||
|
DOORS,GLASS,DOORGlas,DOOR,"Glass doors, manual shopping store doors, glass home front doors. Most store doors would probably fit here.","Bay, Door, French, Front, Glass, Patio, Store"
|
||||||
|
DOORS,HARDWARE,DOORHdwr,DOOR,"Door knobs, door locks, deadbolts, chain locks, push bars. Lock Picking, Doorknob Jiggles.","Bar, Chain, Deadbolt, Door, Doorknob, Fixture, Handle, Hardware, Hinge, Jiggle, Knob, Latch, Lock, Peephole, Picking, Push, Stop"
|
||||||
|
DOORS,HITECH,DOORTech,DOOR,"James bond type doors, something not quite sci-fi but more advanced, Modern lab, or air sealed chamber, vaults. See also SCIFI-DOOR.","7, Bond, Door, Fort, Futuristic, High-Tech, James, Knox, Lab, Modern, Safe, Security, Vault"
|
||||||
|
DOORS,HYDRAULIC & PNEUMATIC,DOORHydr,DOOR,"Self closing doors, lots of office and store doors are of this type. Also automatic doors if hydraulic or pneumatic.","Closer, Dock, Door, Hatch, Hydraulic, Loading, Pneumatic, Powered"
|
||||||
|
DOORS,KNOCK,DOORKnck,DOOR,Knocking on various door surfaces.,"Bang, Knock, Knocker, Rap, Thud, Thump"
|
||||||
|
DOORS,METAL,DOORMetl,DOOR,"Metal doors, like a vault door or heavy door. PRISON doors have their own category, as they are quite specific sounding.","Door, Electrical, Fire, Fire-Door, Garage, Hatch, Panel, Rollup, Screen"
|
||||||
|
DOORS,MISC,DOORMisc,DOOR,Door sounds not fitting another category in this list.,Miscellaneous
|
||||||
|
DOORS,PLASTIC,DOORPlas,DOOR,Doors made out of plastic. Not very common but maybe a door to a child's play house.,"John, Porta"
|
||||||
|
DOORS,PRISON,DOORPrisn,DOOR,"Big metal jail and prison doors. Sliding prison doors, or swinging jail doors.","Cell, Confinement, Correctional, Detention, Gaol, Incarceration, Jail, Lockup, Penitentiary, Solitary"
|
||||||
|
DOORS,REVOLVING,DOORRevl,DOOR,"Classic revolving doors used as entries to hotels, banks, malls.","Airport, Bank, Hotel, Mall, Revolve, Revolving, Rotate, Rotating, Turning"
|
||||||
|
DOORS,SLIDING,DOORSlid,DOOR,"Manual sliding doors, such as a patio door, sliding shower doors. Electric sliding doors to DOORS-ELECTRIC.","Door, Glass, Patio, Shower, Sliding"
|
||||||
|
DOORS,STONE,DOORSton,DOOR,"Mostly a fantasy type door, but think Gates of Moria, big tombstone vault doors.","Castle, Crypt, Entrance, Mausoleum, Secret, Temple, Tomb"
|
||||||
|
DOORS,SWINGING,DOORSwng,DOOR,"Swinging doors, like old-west saloon or church pulpit doors. Doors on swinging springs.","Pulpit, Restaurant, Saloon"
|
||||||
|
DOORS,WOOD,DOORWood,DOOR,"Wood doors, home, school, most general door category probably.","Apartment, Armoires, Barn, Cabin, Church, Closet, Condo, Entrance, Front, Home, House"
|
||||||
|
DRAWERS,METAL,DRWRMetl,DRWR,Metal drawers like on a toolbox.,"Cabinet, Cash, Chest, Filing, Rack, Register, Security, Toolbox, Workbench"
|
||||||
|
DRAWERS,MISC,DRWRMisc,DRWR,Drawer sounds not fitting another category in this list.,Miscellaneous
|
||||||
|
DRAWERS,PLASTIC,DRWRPlas,DRWR,"Plastic drawers, maybe on a parts bin.","Art, Bin, Classroom, Craft, Desk, Garage, Makeup, Organizer, Parts, Plastic, Rolling, Stackable, Storage, Supply"
|
||||||
|
DRAWERS,WOOD,DRWRWood,DRWR,"Kitchen drawers, bathroom drawers. Most drawers would probably fall into this category.","Bathroom, Bedroom, Buffet, Desk, Drawer, Dresser, Kitchen, Nightstand"
|
||||||
|
ELECTRICITY,ARC,ELECArc,ELEC,"A longer movement of electricity, think Jacob's ladder, or a power station switching arc when decoupled.","Arc, Arch, Ark, Carbon, Coil, Discharge, Electrocute, Flash, Gap, Ignition, Jacobs, Ladder, Lamp, Plasma, Power, Pulse, Spark, Station, Surge, Tesla, Welding"
|
||||||
|
ELECTRICITY,BUZZ & HUM,ELECBuzz,ELEC,"Ground hum, buzzes, steady electrical hums, guitar pickup hum, amp hum. Any steady electric hum or buzz.","Amp, Buzz, Buzzing, Capacitor, Circuit, Drone, Elec, Electricity, Electro, Generator, Ground, Hum, Induction, Inductor, Line, Power, Resonance, Speaker, Station, Substation, Transformer, Transmission"
|
||||||
|
ELECTRICITY,ELECTROMAGNETIC,ELECEmf,ELEC,Electromagnetic field recordings specifically.,"Coil, EMF, Field, Induction, Interference, Magnetism, Pickup, Radiation, Radio, Spectrum, Waves"
|
||||||
|
ELECTRICITY,MISC,ELECMisc,ELEC,Electric sounds not fitting another category in this list.,"Current, Elec, Electrically, Electrician, Electricians, Electrics, Electrification, Miscellaneous, Voltage"
|
||||||
|
ELECTRICITY,SPARKS,ELECSprk,ELEC,"Think sparklers, or a downed power line emitting sparks, welding... A singular spark would probably be a ZAP.","Amperage, Burst, Circuit, Crackle, Elec, Electrical, Electrically, Electrician, Electricians, Electrics, Electrification, Influx, Short, Shorting, Sparks, Sputter, Weld, Welding"
|
||||||
|
ELECTRICITY,ZAP,ELECZap,ELEC,"A short, shocking fast percussive electrical sound. Or a repetition of short fast sounds. A taser is a series of ZAPS.","Bolt, Discharge, Electric, Electrocute, Fence, Gun, Jolt, Shock, Stun, Tase, Taser, Triggers, Zapper, Zaps"
|
||||||
|
EQUIPMENT,BRIDLE & TACK,EQUIPBridle,EQUIP,"Equipment for animals use. Horse bridle, reigns, leashes, harnesses, dog leashes, saddle straps.","Bit, Bits, Bridle, Bridles, Chains, Curb, Dog, Guards, Halter, Halters, Harness, Leash, Reins, Saddle, Stirrup, Strap, Tack"
|
||||||
|
EQUIPMENT,HITECH,EQUIPTech,EQUIP,"Hi-Tech gear, things like spacesuits, complex suits with lots of gizmos or complex parts.","Belt, Cloaking, Device, Field, Force, Space, Spyware, Suit, Utility"
|
||||||
|
EQUIPMENT,MISC,EQUIPMisc,EQUIP,Equipment sounds not fitting another category in this list. See also TOOLS and OBJECT categories.,Miscellaneous
|
||||||
|
EQUIPMENT,RECREATIONAL,EQUIPRec,EQUIP,"Recreational gear, like backpacks, hiking gear, rock climbing harnesses, camping tents. Specifically outdoor recreation. See also OBJECTS-GYM.","Backpack, Bag, Bags, Balls, Beach, Camp, Camping, Canoes, Chairs, Climb, Climbing, Club, Clubs, Equipment, Exercise, Fishing, Frisbees, Gear, Golf, Hike, Hiking, Hunting, Jacket, Kites, Life, Mountaineering, Oar, Outfitting, Paddle, Racket, Rackets, Rock, Rods, Ski, Sleeping, Sports, Stove, Tennis, Tent, Tents, Vest"
|
||||||
|
EQUIPMENT,SPORT,EQUIPSprt,EQUIP,"Gear worn while playing sports, American football shoulder pads, hockey gear, etc.","Badminton, Balls, Bars, Baseball, Basketball, Bats, Boxing, Clubs, Cricket, Cycling, Equipment, Football, Gear, Gloves, Golf, Gymnastics, Helmet, Hockey, Hoops, Hurdle, Jock, Knee, Mats, Mitt, Mouthpiece, Pad, Puck, Rackets, Racquet, Shin, Shoulder, Skates, Ski, Snowboards, Soccerball, Squash, Sticks, Strap, Tennis, Volleyballs, Weightlifting, Yoga"
|
||||||
|
EQUIPMENT,TACTICAL,EQUIPTact,EQUIP,"Military and police belts, bandoliers, ammo belts. Military gear, troop gear.","Ammo, Armor, Bandoleer, Belt, Binoculars, Body, Boots, Camouflage, Climbing, Combat, Compass, Duty, Flashlights, Gas, Gear, Gloves, Goggles, Handcuffs, Helmet, Holsters, Mask, Masks, Military, Multi-Tools, Night, Nightstick, Police, Rucksacks, Vests, Vision"
|
||||||
|
EXPLOSIONS,DESIGNED,EXPLDsgn,EXPL,Designed or stylized explosions and sweeteners. Hyperreal explosions.,"Atomic, Bangs, Blast, Blasted, Blasting, Blasts, Blazes, Blowup, Bombe, Bombing, Bombproof, Bombshell, Boom, Booms, Burst, Bursts, Combustions, Detonation, Detonations, Discharges, Echo, Eruptions, Explo, Fragmentary, Grenade, Ignitions, Kiloton, Nuke, Outbursts, Pyrotechnic, Shockwave, Stylized, Sweetener, Thermonuclear"
|
||||||
|
EXPLOSIONS,MISC,EXPLMisc,EXPL,Explosion sounds not fitting another category in this list.,Miscellaneous
|
||||||
|
EXPLOSIONS,REAL,EXPLReal,EXPL,"Real explosion recordings, realistic explosions, not overly stylized. (FIREWORKS has its own category)","Bang, Bangs, Blast, Blasted, Blasting, Blasts, Blazes, Blowup, Bombe, Bombing, Bombproof, Bombshell, Boom, Booms, Burst, Bursts, C4, Combustions, Crack, Detonation, Detonations, Discharges, Dynamite, Eruptions, Explo, Explosive, Fragmentary, Grenade, Ignitions, Implosion, Outbursts, Plastic, Pop, Pyrotechnic"
|
||||||
|
FARTS,DESIGNED,FARTDsgn,FART,"Fake or designed farts, whoopie cushion, flarp, mouth farts.","Armpit, Balloon, Cushion, Fart, Fartbag, Flarp, Flatulence, Poot, Putty, Raspberry, Razzberry, Sludge, Toot, Whoopie"
|
||||||
|
FARTS,MISC,FARTMisc,FART,Fart sounds not fitting another category in this list.,"Animal, Breaking, Fart, Farting, Flatulence, Flatus, Gas, Passing, Poot, Toot, Wind"
|
||||||
|
FARTS,REAL,FARTReal,FART,Real recordings of farts.,"Breaking, Fart, Farting, Flatulence, Flatus, Gas, Human, Passing, Poot, Toot, Wind"
|
||||||
|
FIGHT,BODYFALL,FGHTBf,FGHT,"Bodyfalls, but probably also body scuffles on ground, from simple to complex. On any surface.","Body, Bodyfall, Collapse, Dive, Drag, Drop, Fall, Falling, Flop, Impact, Nosedive, Plunge, Scuffle, Slam, Slip, Slump, Tackle, Thud, Topple, Tumble"
|
||||||
|
FIGHT,CLOTH,FGHTClth,FGHT,"Active or big scuffling cloth, big fight cloth movement.","Arts, Brawl, Combat, Flutter, Grab, Grapple, Grasp, Hit, Judo, Martial, Movement, Rustle, Scuffling, Tackle, Tug, Tussle, Twist"
|
||||||
|
FIGHT,GRAB,FGHTGrab,FGHT,"Big arm grabs, hand or arm blocks as well as they're similar sounds, grappling during wrestling or martial arts.","Catch, Cling, Clutch, Combat, Embrace, Grapple, Grappling, Grasp, Grip, Hand, Hold, Hook, Hug, Judo, Nab, Pinch, Pluck, Seize, Snag, Snatch, Take, Wrestle"
|
||||||
|
FIGHT,IMPACT,FGHTImpt,FGHT,"Fight impacts, blocks, kicks and punches. BODYFALLS has its own subcategory here, as do GRABS.","Bang, Biff, Blow, Blows, Body, Bouts, Brawl, Brawling, Counterpunch, Elbows, Fisticuffs, Fists, Haymakers, Hit, Hits, Jab, Jabs, Kick, Knock, Knockdown, Knockout, Knockouts, Knocks, Punch, Ram, Roundhouse, Shots, Shove, Slam, Slap, Slaps, Smack, Sock, Strike, Strikes, Takedown, Thwack, TKO, Uppercut, Wallop, Whack"
|
||||||
|
FIGHT,MISC,FGHTMisc,FGHT,Fight sounds not fitting another category in this list.,"Attacking, Battling, Beating, Bout, Brawling, Clash, Combatant, Dispute, Miscellaneous, Quarreling, Sparring"
|
||||||
|
FIRE,BURNING,FIREBurn,FIRE,"Covers normal fire burning, mostly roars, not isolate fire crackle, which have their own category. See also new FIRE-TURBULENT category.","Afire, Aflare, Alight, Arson, Blaze, Blazing, Bonfire, Brush, Burning, Campfire, Char, Combustion, Conflagration, Consume, Consumption, Cremation, Deflagration, Enflame, Fiery, Flames, Flare, Flashpoint, Forest, Hotspot, Incinerate, Incineration, Incinerator, Inferno, Kindle, Kindling, Pyre, Roast, Scorch, Scorching, Sear, Singe, Smoking, Smolder, Smoldering, Structure, Tinder, Wildfire"
|
||||||
|
FIRE,BURST,FIREBrst,FIRE,"A sharp and loud release of fire, covers fire bursts, big flare ups. Think a backdraft explosion of fire. A flamethrower. Usually a single event.","Backdraft, Blast, Burst, Bursting, Combustion, Detonate, Discharge, Discharged, Discharging, Dragon, Engulf, Eruption, Exploded, Explosion, Fiery, Fire, Fireball, Flare-Up, Flash, Inflaming, Outburst, Over, Release"
|
||||||
|
FIRE,CRACKLE,FIRECrkl,FIRE,"Specifically crackly fire elements, isolated crackle, or a very crackly fire, good for layering to other fire.","Crack, Crackling, Crackly, Crinkle, Pop, Popping, Snap, Snapping, Snappy, Sparkle, Spit"
|
||||||
|
FIRE,GAS,FIREGas,FIRE,Gas burners as you'd find in a kitchen stove or gas grill. That sort of hissy or airy fire burn. Crematoriums and furnaces.,"Acetylene, Blowtorch, Bunsen, Burner, Butane, Camping, Cookstove, Crematorium, Diesel, Flamethrower, Fuel, Furnace, Gas, Gasoline, Grill, Kerosene, Kitchen, LP, Methane, Natural, Oilstove, Petrol, Propane, Range, Stove"
|
||||||
|
FIRE,IGNITE,FIREIgn,FIRE,"Fire ignites, including matches, lighters. That initial spark of ignition of a fire.","Ablaze, Aflame, Arouse, Arson, Asbestos, Combust, Combusted, Combustible, Conflagrate, Emblaze, Enflame, Erupt, Fanned, Fanning, Fire, Flame, Flammable, Ignitable, Ignite, Ignited, Igniter, Ignitor, Incite, Inflame, Inflammable, Initiate, Light, Lighter, Match, Reignite, Set, Spark, Stimulate, Trigger, Up, Zippo"
|
||||||
|
FIRE,MISC,FIREMisc,FIRE,Fire sounds not fitting another category in this list.,"Cinder, Cinders, Extinguisher, Miscellaneous"
|
||||||
|
FIRE,SIZZLE,FIRESizz,FIRE,"Glowing red pokers, fuses, coal embers when extinguished, sauna rocks when water is poured on them. Glowing red hot.","Barbecue, Barbeque, Burn, Crackling, Fizzing, Frying, Fuse, Hissing, Pan, Poker, Popping, Searing, Sizzle, Sizzled, Sizzling, Sparkling"
|
||||||
|
FIRE,TORCH,FIRETrch,FIRE,"Constant torches and torch movement, that phasey fire sound as a torch is moved around.","Beacon, Blazing, Brazier, Candlelight, Cresset, Firebrand, Firestick, Flambeau, Flaming, Flare, Flickering, Lamplighter, Sconce, Signal, Smothered, Smothering, Stick, Taper, Torchbearer, Torcher, Torching, Torchlight, Wick"
|
||||||
|
FIRE,TURBULENT,FIRETurb,FIRE,"Wildly whooshing turbulent violent fire. Larger than just burning, think fire tornado. Extreme moving fire.","Backdraft, Blaze, Engulf, Fierce, Fierceness, Firestorm, Flame, Fury, Inferno, Swirl, Thrower, Tornado, Violence, Violent"
|
||||||
|
FIRE,WHOOSH,FIREWhsh,FIRE,Big whooshy fire events like a fireball through the air.,"Backdraft, Blaze, Fireball, Flare, Molotov, Rush, Whooshed, Whooshing"
|
||||||
|
FIREWORKS,COMMERCIAL,FRWKComr,FRWK,"Large commercial fireworks, mortars, as on the 4th of July. Full displays or single large mortars.","Aerial, Boom, Display, Firework, Independence, Kaboom, Mortar, Pyrotechnic, Pyrotechnical, Pyrotechnics, Rockets, Salute, Shell, Shells"
|
||||||
|
FIREWORKS,MISC,FRWKMisc,FRWK,Firework sounds not fitting another category in this list.,Miscellaneous
|
||||||
|
FIREWORKS,RECREATIONAL,FRWKRec,FRWK,"Small store-bought, recreational fireworks, firecrackers and roman candles and bottle rockets.","Bang, Blast, Bomb, Boom, Bottle, Candle, Cherry, Crackle, Firecracker, Fizz, Fountain, Jack, Jumping, M80, Pop, Popper, Rocket, Roman, Smoke, Snaps, Sparkler, Whizz"
|
||||||
|
FOLEY,CLOTH,FOLYClth,FOLY,Used for 'performed' official and very clean Foley done to picture. For your own wild recordings use FOOTSTEPS and OBJECT categories.,"Cape, Clothing, Dress, Fabric, Flap, Flutter, Jacket, Movement, Pants, Rustle, Shirt, Shorts, Skirt, Textile, Zuzz"
|
||||||
|
FOLEY,FEET,FOLYFeet,FOLY,Used for 'performed' official and very clean Foley done to picture. For your own wild recordings use FOOTSTEPS and OBJECT categories.,"Foot, Footstep, Footsteps, Marching, Running, Scuff, Scuffling, Shoe, Sneaking, Sprinting, Step, Stomping, Surface, Tiptoeing, Trudging, Walking"
|
||||||
|
FOLEY,HANDS,FOLYHand,FOLY,Used for 'performed' official and very clean Foley done to picture. For your own wild recordings use FOOTSTEPS and OBJECT categories.,"Clapping, Flicking, Grab, Grasping, Handle, Pat, Patting, Rubbing, Scratching, Set, Shaking, Slapping, Snapping, Touching"
|
||||||
|
FOLEY,MISC,FOLYMisc,FOLY,Used for 'performed' official and very clean Foley done to picture. For your own wild recordings use FOOTSTEPS and OBJECT categories.,Miscellaneous
|
||||||
|
FOLEY,PROP,FOLYProp,FOLY,Used for 'performed' official and very clean Foley done to picture. For your own wild recordings use FOOTSTEPS and OBJECT categories.,"Objects, Prop, Props"
|
||||||
|
FOOD & DRINK,COOKING,FOODCook,FOOD,"Things overtly related to cooking, baking, preparation, chopping.","Baked, Baking, Barbecuing, Blending, Boil, Boiled, Boiling, Broiling, Burning, Buttering, Canning, Caramelization, Catering, Chopping, Cookery, Cooking, Cookpot, Cuisine, Culinary, Decorating, Defrosting, Dicing, Fry, Gourmet, Grill, Grilling, Mixing, Pantry, Parboiling, Precooking, Preparation, Prepare, Preparing, Prepping, Recipe, Recipes, Reheating, Roasting, Salting, Sauteing, Sautéing, Saut√©, Seasoning, Sizzle, Slicing, Steam, Steaming, Stew, Stir, Stovetop, Whisking"
|
||||||
|
FOOD & DRINK,DRINKING,FOODDrnk,FOOD,"Specifically drinking, would also cover animals drinking, dogs lapping water, a horse drinking.","Alcohol, Ale, Beer, Beverage, Beverages, Booze, Boozing, Brew, Chai, Champagne, Chug, Chugging, Cider, Cocktail, Coffee, Coke, Cola, Consumed, Consuming, Consummation, Consumption, Drink, Drinkable, Drinker, Drinkers, Gatorade, Gin, Glug, Gulp, Gulping, Guzzling, Imbibe, Imbibing, Intoxication, Juice, Kombucha, Lemonade, Liquor, Liquoring, Margarita, Martini, Milk, Rum, Sip, Sipping, Slurp, Slurping, Soda, Swallow, Swallowing, Swig, Swilling, Tea, Tequila, Thirst, Thirsty, Tippling, Vodka, Whiskey, Wine"
|
||||||
|
FOOD & DRINK,EATING,FOODEat,FOOD,"Specifically eating, would also cover animals eating, a dog eating or a horse eating. Swallowing, chewing.","Appetizer, Bite, Biting, Chew, Chewing, Chomp, Chow, Chowing, Consume, Consuming, Consumption, Crunch, Devouring, Digesting, Dine, Eat, Eaten, Edible, Feast, Feasting, Feed, Feeding, Feeds, Foods, Foodstuffs, Gobbling, Gorging, Gourmet, Grocery, Grub, Gullet, Hungry, Ingest, Ingesting, Ingestion, Lunch, Meal, Meals, Munch, Munching, Nibbling, Noshing, Nourishment, Nutrition, Nutritional, Partaking, Snack, Snacking, Supper, Swallow, Swallowing"
|
||||||
|
FOOD & DRINK,GLASSWARE,FOODGware,FOOD,"Glass and bottle movement, specifically at a restaurant or bar. Isolated glassware recordings.","Beaker, Beakers, Beer, Beverages, Bottle, Bottles, Carafe, Carafes, Champagne, Clink, Cup, Decanters, Flasks, Flute, Glass, Glasswares, Glasswork, Goblet, Goblets, Highball, Jug, Martini, Mug, Mugs, Pyrex, Shot, Stemware, Teacups, Tumbler, Vase, Vases, Wine, Wineglass, Wineglasses"
|
||||||
|
FOOD & DRINK,INGREDIENTS,FOODIngr,FOOD,"Nuts, seeds, grains, things like that. Raw food recordings often used for other design purposes, vegetable recordings. See also GORE-SOURCE.","Batter, Beans, Bread, Canned, Cereal, Cheese, Condiments, Contents, Cornstarch, Egg, Eggs, Flour, Food, Fruit, Ghee, Grain, Groceries, Herb, Herbs, Juice, Lard, Lentils, Maize, Meat, Milk, Nuts, Oats, Oleo, Pasta, Pepper, Preserves, Roux, Salt, Seeds, Spices, Suet, Sugar, Treacle, Vanilla, Vegetables, Yeast"
|
||||||
|
FOOD & DRINK,KITCHENWARE,FOODKware,FOOD,"Pots and pans movement, big metal bowls, prep bowls.","Bakeware, Baking, Board, Bowl, Bread, Can, Chef's, Chopping, Colander, Cookie, Cookware, Corkscrew, Cup, Cutting, Dish, Fork, Frying, Grater, Kitchen, Knife, Knives, Ladle, Measuring, Mixing, Opener, Pan, Pans, Paring, Peeler, Pin, Pot, Pots, Rolling, Saucepan, Saucepans, Scissors, Serving, Shears, Sheet, Slotted, Spatula, Spoon, Spoons, Steamer, Stewpan, Stockpot, Strainer, Teakettle, Tongs, Utensil, Utility, Vegetable, Whisk, Wooden"
|
||||||
|
FOOD & DRINK,MISC,FOODMisc,FOOD,Food and Drink sounds not fitting another category in this list.,"Banquet, Diet, Dieting, Epicurean, Fare, Garnish, Meal, Miscellaneous"
|
||||||
|
FOOD & DRINK,POUR,FOODPour,FOOD,"Pouring liquid, water, beer, wine, champagne, etc. Some overlap with WATER-POUR, but specifically consumption liquids.","Alcohol, Ale, Bartender, Beer, Chai, Champagne, Cider, Cocktail, Coffee, Coke, Cola, Flow, Flowing, Gatorade, Gin, Juice, Kombucha, Lemonade, Margarita, Martini, Milk, Pour, Pouring, Rum, Serve, Slosh, Soda, Spill, Tea, Tequila, Vodka, Whiskey, Wine"
|
||||||
|
FOOD & DRINK,TABLEWARE,FOODTware,FOOD,"Silverware and/or plate movement, in a restaurant or dinner at home. GLASSWARE it's own subcategory.","Bowl, Bowls, Coasters, Cups, Cutlery, Dessert, Dinner, Dinnerware, Dishes, Flatware, Fork, Glasses, Knife, Mats, Napkin, Pepper, Place, Plates, Platter, Platters, Rings, Salad, Salt, Saucer, Saucers, Serving, Shaker, Silverware, Soup, Spoon, Sugar, Tablespoon, Teapots, Teaspoon, Tongs, Tray, Trays, Tureens, Utensils"
|
||||||
|
FOOTSTEPS,ANIMAL,FEETAnml,FEET,"Individual animal footsteps, dog, elephant. Group animals like a herd or stampede to MOVEMENT-ANIMALS. See also FOOTSTEPS-HORSE,","Bolt, Buck, Clack, Claws, Clomp, Footfall, Footprints, Hoof, Hooves, Jump, Lope, March, Pace, Patter, Paw, Plod, Prance, Rear, Run, Scuff, Scurry, Scurrying, Shuffle, Skip, Sprint, Stamp, Stampede, Step, Steps, Stomp, Stride, Strut, Stumble, Thud, Tracks, Tramp, Tread, Walk"
|
||||||
|
FOOTSTEPS,CREATURE,FEETCrea,FEET,"Monster feet, Put dinosaur feet in here too, but also for small fantasy creatures.","Alien, Beast, Clack, Clomp, Creature, Dinosaur, Dragon, Footfall, March, Monster, Mythical, Pace, Patter, Plod, Scuff, Shuffle, Stamp, Step, Stomp, Stride, Thud, Tramp, Tread"
|
||||||
|
FOOTSTEPS,HORSE,FEETHors,FEET,"All manner of horse feet, from single to running.","Canter, Cantering, Clack, Clip-Clop, Clomp, Clop, Clopping, Footfall, Gallop, Hoof, Hooves, March, Pace, Patter, Plod, Run, Scuff, Shuffle, Stamp, Step, Stomp, Stride, Thud, Tramp, Tread, Trot, Trots, Trotting, Walk"
|
||||||
|
FOOTSTEPS,HUMAN,FEETHmn,FEET,"Human feet, any surface, barefoot, shoes, it all goes here.","Ambling, Clack, Climb, Clomp, Feet, Foot, Footfall, Footstep, Gallivanting, Hike, Hiking, Hobble, Hobbling, Hop, Jog, Jump, Limp, Limping, March, Marching, Meandering, Moseying, Pace, Pacing, Pad., Patter, Perambulating, Plod, Plodding, Promenading, Rambling, Roaming, Run, Running, Sashaying, Saunter, Sauntering, Schlepping, Scuff, Scuffling, Shambles, Shuffle, Shuffling, Skip, Sprint, Stamp, Step, Stepping, Stomp, Stomping, Stride, Striding, Stroll, Strolling, Swaggering, Thud, Tip-Toe, Tiptoe, Traipsing, Tramp, Tramping, Tread, Treading, Trekking, Trudging, Walk, Walking, Wandering"
|
||||||
|
FOOTSTEPS,MISC,FEETMisc,FEET,Footstep sounds not fitting another category in this list.,"Clack, Clomp, Footfall, March, Miscellaneous, Pace, Patter, Plod, Scuff, Shuffle, Stamp, Step, Stomp, Stride, Thud, Tramp, Tread"
|
||||||
|
GAMES,ARCADE,GAMEArcd,GAME,"Not for arcade ambience, which would go under AMBIENCE-AMUSMENT, but for specific Pinball machines, upright video games, games of skill like Skeeball.","Air, Claw, Coin-Op, Crane, Dance, Foosball, Gallery, Game, Games, Hockey, Machine, Pacman, Pinball, Retro, Revolution, Shooting, Skee-Ball, Skeeball, Video, Whack-A-Mole"
|
||||||
|
GAMES,BOARD,GAMEBoard,GAME,"Board games with pieces, think Monopoly, Risk, chess, checkers.","Backgammon, Bingo, Boardgame, Checkerboard, Checkers, Chess, Chessboard, Clue, Cribbage, Dominoes, Game, Gameboard, Go, Mahjong, Mancala, Monopoly, Pieces, Risk, Roleplaying, Scrabble, Uno"
|
||||||
|
GAMES,CASINO,GAMECas,GAME,"Casino slot machines, Keno machines, roulette, craps.","Baccarat, Betting, Blackjack, Cards, Craps, Dice, Keno, Machine, Machines, Money, Poker, Roulette, Shuffling, Slot, Video, Wheel"
|
||||||
|
GAMES,MISC,GAMEMisc,GAME,Game sounds not fitting another category in this list. Cards would go here. Jenga.,"a, Bag, Ball, Balloon, Basketball, Bean, Bottle, Bowl, Bowler, Break, Can, Clown, Coin, Crazy, Dart, Darts, Duck, Fish, Frog, Game, Goblet, High, Hoop, Knock-Down, Knockdown, Lucky, Milk, Miscellaneous, Number, Plate, Plinko, Pond, Prize, Ring, Roller, Roulette, Shoot, Shooting, Skee-Ball, Star, Strength, Striker, Test, the, Throw., Toss, Up, Water, Whack-a-Mole, Wheel, Wiffle, Your"
|
||||||
|
GAMES,VIDEO,GAMEVideo,GAME,"Console video games from Atari to PS5, designed fake video games for consoles like Playstation.","360, 64, Atari, Console, Fortnite, Gameboy, Halo, Megadrive, Minecraft, Nintendo, One, Pacman, Playstation, Pong, PS4, PS5, PSP, Sega, SNES, Xbox, Zelda"
|
||||||
|
GEOTHERMAL,FUMAROLE,GEOFuma,GEO,"Geothermal steam vent. A hole, crack or fizzure releasing gases.","Fissure, Fumarole, Fumarolic, Gaseous, Geothermal, Hydrothermal, Solfatara, Steam, Steam Vent, Stufa, Sulfur, Sulphur, Thermal, Vent, Venthole, Vents, Volcanic, Yellowstone"
|
||||||
|
GEOTHERMAL,GEYSER,GEOGeyser,GEO,"Geysers and eruptions, as you'd find in Yellowstone.","Basin, Blowhole, Boil, Boiling, Bubble, Bubbler, Erupt, Erupting, Eruption, Faithful, Geiser, Geothermal, Geothermic, Geyser, Geysir, Old, Scalding, Spout, Spouter, Spray, Strokkur, Thermal, Yellowstone"
|
||||||
|
GEOTHERMAL,LAVA,GEOLava,GEO,"Lava flows, crackles, could be some overlap with FIRE, but overtly lava sounding things go here.","Basaltic, Basalts, Calderas, Cindery, Crackle, Craters, Fireballs, Flare, Flow, Igneous, Kilauea, Lava, Lave, Magma, Magmas, Magmatic, Molten, Pumices, Pyroclastic, Pyroclasts, Viscous, Volcan, Volcanically, Volcaniclastic, Volcanics, Volcano, Volcanoes, Volcanos, Vulcanian, Vulcanic, Vulcanism, Yellowstone"
|
||||||
|
GEOTHERMAL,MISC,GEOMisc,GEO,Geothermal sounds not fitting another category in this list.,"Bubbling, Geotherm, Geothermic, Hot, Hydroelectric, Hydroelectricity, Hydrogeologic, Hydrogeological, Hydrogeology, Hydronic, Hydropower, Hydrovolcanic, Hypothermal, Pool, Spring, Thermal, Thermic, Thermodynamic, Thermoelectric, Thermogenetic, Thermogenic, Thermogenous, Thermogeological, Thermological, Thermosyphon, Thermosyphons, Thermotic, Volcanogenic, Volcanological, Yellowstone"
|
||||||
|
GEOTHERMAL,MUD POTS,GEOMudpot,GEO,"Mudpots and paint pots, popping liquidy mud sounds.","Bubble, Bubbling, Glop, Gurgling, Mud, Paint, Pit, Pot, Tar, Yellowstone"
|
||||||
|
GLASS,BREAK,GLASBrk,GLAS,"Glass singular breaks, so a bottle smash, or a window shatter.","Break, Bulletproof, Burst, Chip, Crack, Crush, Demolish, Destroy, Fracture, Fragment, Frosted, Glass, Laminated, Safety, Shatter, Smash, Snap, Splinter, Split, Stained, Tempered, Tinted, Window"
|
||||||
|
GLASS,CRASH & DEBRIS,GLASCrsh,GLAS,"Glass crashes and debris, so multiple items crashing, and isolated glass shards and debris.","Broken, Bulletproof, Crash, Debris, Fragment, Fragments, Frosted, Laminated, Pieces, Remains, Rubble, Safety, Shard, Shards, Shattered, Splinters, Stained, Tempered, Tinted, Window"
|
||||||
|
GLASS,FRICTION,GLASFric,GLAS,"Glass friction, such as a scrape or squeak. Could be some overlap with GLASS-TONAL.","Abrasion, Bulletproof, Creak, Frosted, Grinding, Laminated, Rasping, Rubbing, Safety, Scrape, Scraping, Scratching, Screech, Screeching, Scuffing, Sliding, Squeak, Squeaking, Stained, Stress, Tempered, Tinted, Window"
|
||||||
|
GLASS,HANDLE,GLASHndl,GLAS,"Glass objects being handled, grabbing a bottle or glass. Some overlap with GLASS-MOVEMENT.","Bulletproof, Catch, Clasp, Clench, Clutch, Down, Embrace, Frosted, Grab, Grapple, Grasp, Grip, Handle, Hold, Laminated, Operate, Pickup, Pull, Safety, Seize, Set, Stained, Take, Tempered, Throw, Tinted, Toss, Use, Window"
|
||||||
|
GLASS,IMPACT,GLASImpt,GLAS,"Glass impacts without breaking, so a bottle hit that doesn't break, a bottle dropping to the ground but not breaking.","Bang, Banging, Blow, Bulletproof, Clink, Collide, Colliding, Drop, Frosted, Hit, Hitting, Impact, Impacting, Jolt, Knock, Laminated, Pound, Ram, Safety, Slam, Slamming, Smack, Smacking, Stained, Strike, Striking, Tempered, Thud, Tinted, Window"
|
||||||
|
GLASS,MISC,GLASMisc,GLAS,Glass sounds not fitting another category in this list.,"Bulletproof, Frosted, Laminated, Miscellaneous, Safety, Stained, Tempered, Tinted, Window"
|
||||||
|
GLASS,MOVEMENT,GLASMvmt,GLAS,"Glass movement, such as a case of bottles rattling, or a bottle rolling on the floor. See also GLASS-HANDLE.","Bulletproof, Chattering, Clanking, Clatter, Frosted, Jangle, Jiggling, Laminated, Ping, Rattle, Rattling, Roll, Rolled, Rolling, Safety, Shaking, Shifting, Sliding, Stained, Swaying, Swinging, Tempered, Tinted, Vibrating, Window"
|
||||||
|
GLASS,TONAL,GLASTonl,GLAS,"Glass tonal sounds, like bowed blass, a glass harmonica, or a bowed crystal bowl.","Blow, Bottle, Bowed, Bowl, Bulletproof, Crystal, Frequency, Frosted, Glass, Harmonic, Harmonica, Harmonics, Laminated, Melodic, Melodious, Musical, Ping, Pitch, Resonance, Resonant, Resonate, Ring, Safety, Shing, Sonorous, Sound, Stained, Tempered, Timbre, Tinkle, Tinkling, Tinted, Tonal, Tone, Window"
|
||||||
|
GORE,BLOOD,GOREBlood,GORE,"Gore blood sounds, dripping and flowing. Likely some overlap with LIQUID & MUD, but this for particularly 'gory' versions.","Artery, Bleed, Bleeding, Blood, Bloodborne, Bloodshed, Bloody, Cells, Circulatory, Clot, Coagulate, Drip, Flow, Globular, Gush, Hemoglobin, Hemorrhage, Ichor, Lifeblood, Plasma, Platelets, Red, Sanguine, Seep, Serum, Spray, Spurt, Transfusion, Vascular, White, Wound"
|
||||||
|
GORE,BONE,GOREBone,GORE,"Gore bones, bones crunching, snapping, break. Put creature JAW SNAPS here too.","Bone, Bonelike, Bonemeal, Boney, Bony, Break, Broken, Cartilages, Compound, Crunch, Femoral, Femur, Fracture, Gristle, Humerus, Jaw, Jawbone, Knuckle, Knucklebone, Ligament, Marrow, Rib, Shatter, Shinbone, Shinbones, Skeletal, Skeleton, Snap, Spinal, Spine, Tibia, Tooth"
|
||||||
|
GORE,BURN,GOREBurn,GORE,"Gore flesh sizzling, possible overlap with FIRE-SIZZLING, but this more exaggerated for flesh burning.","Acid, Blistered, Brand, Burned, Cauterize, Cauterized, Char, Charred, Peeling, Scald, Scalds, Scorch, Scorched, Sear, Searing, Singe, Singed, Sizzle"
|
||||||
|
GORE,FLESH,GOREFlsh,GORE,"Gore flesh tearing, ripping, peeling. Very graphic flesh sounds. A monster ripping someone apart.","Carcass, Carnal, Corpse, Corpses, Decaying, Flayed, Flaying, Flesh, Fleshiness, Fleshly, Fleshy, Gnaw, Gristle, Intestine, Meat, Muscle, Peel, Putrefying, Rend, Rip, Rotting, Sinew, Sinews, Sinewy, Skin, Skinless, Skinned, Skins, Tear, Tissue, Wound"
|
||||||
|
GORE,MISC,GOREMisc,GORE,Gore sounds not fitting another category in this list.,"Beheadings, Bloodbaths, Carnage, Decapitations, Disembowelment, Dismemberment, Dismemberments, Gore, Gory, Graphic, Gruesome, Gruesomeness, Macabre, Miscellaneous, Mutilation, Offal, Remains, Scalpings, Slaughter, Viscera"
|
||||||
|
GORE,OOZE,GOREOoze,GORE,"Gore gross oozing liquid, open wounds, possible overlap with LIQUID categories, but this hyperreal and over the top.","Blob, Burbling, Bursting, Congeal, Congealed, Congealing, Discharge, Drippy, Emanate, Exude, Exuded, Exudes, Exuding, Flow, Gelatin, Gobs, Goo, Goopy, Guck, Gunk, Gushes, Jelly, Liquidy, Muck, Mucus, Mush, Ooze, Oozy, Permeate, Pus, Putrefy, Putrefying, Putrescence, Putrescent, Putrid, Seep, Seeped, Seeping, Seeps, Slime, Sliminess, Slithering, Sludge, Slurry, Spewing, Splattered, Spurting, Squishy, Suppurate, Suppurating, Syrup, Viscous"
|
||||||
|
GORE,SOURCE,GORESrce,GORE,"Recordings meant as source for GORE sounds, smashing a watermelon, stabbing a pumpkin, etc.","Arteries, Bones, Brain, Cartilage, Cord, Entrails, Eyeballs, Guts, Intestines, Ligaments, Limbs, Matter, Muscles, Organs, Raw, Skin, Skull, Source, Spinal, Teeth, Tendons, Veins"
|
||||||
|
GORE,SPLAT,GORESplt,GORE,"Gore splats, gross splats of gooey stuff. A body or head exploding and hitting the wall. Hyperreal.","Gack, Glob, Globs, Glop, Goo, Goop, Goosh, Intestine, Juicy, Organ, Plop, Plopping, Plops, Poop, Smear, Snot, Spack, Spattered, Spatters, Spittle, Splatter, Splattered, Splatters, Splatting, Splodge, Splodges, Splosh, Splotch, Splotched, Splotches, Splotching, Squish"
|
||||||
|
GORE,SQUISH,GORESqsh,GORE,"Gore squishes, disgusting squishes or flesh and organs. A dinosaur stepping on a person and squishing them.","Compress, Goosh, Intestine, Juicy, Macerate, Mash, Mashed, Mashing, Moosh, Mush, Mushed, Mushy, Organ, Slop, Splat, Squash, Squashed, Squashing, Squashy, Squeezed, Squeezes, Squeezing, Squelch, Squelchy, Squidge, Squiggle, Squirty, Squish, Squishing, Squishy"
|
||||||
|
GORE,STAB,GOREStab,GORE,Gore sharp stabs and sword pulls. That classic sound of someone being impaled. More crunchy than a SQUISH.,"Arrow, Behead, Cut, Dagger, Decapitate, Disembowel, Dismember, Eviscerate, Flay, Impale, Impaling, Jab, Jabbed, Jabbing, Jugular, Knife, Knifed, Knifing, Mutilate, Needle, Penetrate, Pierce, Poke, Prod, Puncture, Shank, Skewer, Slash, Slashing, Slice, Spear, Spike, Stab, Sword, Syringe, Thrust, Vorpal"
|
||||||
|
GUNS,ANTIQUE,GUNAntq,GUN,"Old black powder weapons, muskets.","Arquebus, Arquebuses, Black, Blunderbuss, Breechloader, Breechloaders, Breechloading, Caplock, Carabine, Civil, Derringer, Derringers, Firearms, Firelock, Flintlock, Gatling, Gun, Gunflint, Harquebus, Historical, Lockplate, Lockwork, Long, Matchlock, Matchlocks, Musket, Musketeer, Musketry, Muskets, Muzzleloader, Muzzleloaders, Muzzleloading, Old, Old-Fashioned, Pepperbox, Powder, Powderhorn, Revolutionary, Rifle, Sixgun, Sixguns, Traditional, Victorian, Vintage, War, West, Wheellock, WWI, WWII"
|
||||||
|
GUNS,ARTILLERY,GUNArtl,GUN,"Modern big guns, guns that launch shells. Not CANNONS which have their own group.","Anti-Aircraft, Anti-Tank, Antiaircraft, Antiarmor, Antitank, Armaments, Artillerie, Artillery, Ballistics, Bazooka, Cannon, Field, Firepower, Heavy, Howitzer, Howitzers, Incoming, Launcher, Light, Missile, Mortar, Mortars, Munitions, Naval, Ordnance, Rocket, Shellfire, Shelling, Tank, Volley, Weapons"
|
||||||
|
GUNS,AUTOMATIC,GUNAuto,GUN,"Fully automatic capable weapons, even when firing singly. Machine guns.","Ak-47, AK47, Automatic, Firearms, Full-Auto, Gatlin, Gatling, Gun, Kalashnikov, M16, M1919, M249, Machine, Maxim, Minigun, Sten, Submachine, Submachinegun, Tommy, Uzi"
|
||||||
|
GUNS,CANNON,GUNCano,GUN,"Pirate and Civil War cannons, for modern artillery see GUNS-ARTILLERY.","Ball, Barrel, Black, Blast, Cannonade, Cannonades, Cannonball, Cannonballs, Cannoneer, Cannoneers, Cannonry, Fuse, Gunpowder, Pirate, Powder, Primer, Projectile, Shot, Siege"
|
||||||
|
GUNS,HANDLE,GUNHndl,GUN,"Alongside GUN-MECHANISM, but specifically gun handling, grabs, holstering, dropping, catching.","Catch, Chambering, Clearing, Clip, Cock, Cocking, Discharge, Draw, Drawing, Drop, Dry-Fire, Dry-Firing, Ejecting, Grab, Grasp, Grip, Gun, Gunstock, Handgrip, Handle, Hold, Holster, Holstering, Load, Loading, Throw, Toss, Unloading"
|
||||||
|
GUNS,HITECH,GUNTech,GUN,"Not quite a sci-fi Laser Gun, but something more advanced than a normal gun, a rail gun for example.","7, Biometric, Bond, Firearms, Gun, James, Night, Rail, Scope, Smart, Spy, Vision"
|
||||||
|
GUNS,MECHANISM,GUNMech,GUN,"Gun various mechanical sounds, clips, cocking, bolt slides.","Assembly, Barrel, Bolt, Brake, Bullet, Buttplate, Chamber, Clip, Cock, Cocking, Cylinder, Firearms, Firing, Flash, Grip, Guard, Gun, Hammer, Loading, Magazine, Mechanics, Mechanism, Muzzle, Pin, Pistol, Rattle, Release, Safety, Sear, Slide, Stock, Stop, Suppressor, Trigger"
|
||||||
|
GUNS,MISC,GUNMisc,GUN,Gun sounds not fitting another category in this list. BULLETS has its own category.,"Air, Firearms, Flare, Paintball"
|
||||||
|
GUNS,PISTOL,GUNPis,GUN,Handguns and pistols.,"&, 9mm, Beretta, Browning, Colt, Double-Action, Firearms, Glock, Handgun, Handguns, Luger, Pistol, Pistole, Pistolet, PPK, Revolver, Revolvers, Ruger, Sauer, Semi-Automatic, Sidearm, Sig, Single-Action, Six-Shooter, Smith, Starter, Walther, Wesson"
|
||||||
|
GUNS,RIFLE,GUNRif,GUN,"Rifles, single shot and semi-automatic.","AK-47, AR-15, Arisaka, Assault, Bolt-Action, Breechloader, Carabine, Carbine, Carbines, Casull, Firearms, Garand, Gun, Hunting, Lever-Action, Long, Luger, M1, M16, Mauser, Powell, Remington, Riffle, Rifle, Rifleman, Riflemen, Riflery, Rimfire, Ruger, Semi-Automatic, Sniper, Sporting, Winchester"
|
||||||
|
GUNS,SHOTGUN,GUNShotg,GUN,"Shotguns, single, dual, pump.","Barrel, Break-Action, Coach, Double, Double-Barreled, Gun, Mossberg, Over-Under, Pump-Action, Sawed-Off, Scattergun, Shotgun, Slug, Winchester"
|
||||||
|
GUNS,SUPPRESSED,GUNSupr,GUN,"Silenced Weapons, silencers.","7, Assassin, Bond, James, Rifle, Silenced, Silencer, Sniper, Spy"
|
||||||
|
HORNS,AIR POWERED,HORNAir,HORN,"Air horns, as in handheld compressed air horns, huge factory air horns. A fog horn. BOATS-HORN has it's own category for ship and ferry horns, as does TRAINS-HORN.","Air, Factory, Fog"
|
||||||
|
HORNS,CELEBRATION,HORNCele,HORN,"Party horns, from small party horns to vuvuzelas.","Bugle, Ceremonial, Fanfare, Festive, Heraldic, Horns, Party, Processional, Toot, Triumphal, Trumpets, Victory, Vuvuzela"
|
||||||
|
HORNS,MISC,HORNMisc,HORN,"Horn sounds not fitting another category in this list, for example hunting calls. Not brass musical instruments which has it's own category in MUSICAL.","Bugle, Call, Duck, Elk, Fox, Hunting"
|
||||||
|
HORNS,TRADITIONAL,HORNTrad,HORN,"Rams horns, conches, alpine horns, old traditional horns, medieval battle horns.","Alpenhorn, Alphorn, Alpine, Battle, Bugle, Conch, Hunting, Shell, Shofar, War"
|
||||||
|
HUMAN,BLOW,HMNBlow,HMN,"Blowing air, blowing up a balloon, blowing out candles, etc. See also HUMAN-BREATH for less forceful breathing.","Adult, Blow, Child, Female, Male, Man, Person, Woman"
|
||||||
|
HUMAN,BREATH,HMNBrth,HMN,"Breathing, normal to heavy, big inhales, sighs.","Adult, Asphyxiate, Asthma, Breath, Breathing, Breaths, Child, Exhalation, Exhale, Exhaled, Exhales, Exhaling, Female, Gasp, Gasping, Heave, Huff, Huffing, Hyperventilate, Hyperventilating, Inhalation, Inhale, Inhaled, Inhales, Inhaling, Labored, Male, Man, Meditation, Pant, Panting, Person, Puffing, Respiration, Sigh, Suffocate, Suffocates, Suffocating, Wheeze, Wheezing, Whoop, Woman"
|
||||||
|
HUMAN,BURP,HMNBurp,HMN,"Human belches, burps.","Adult, Belch, Belching, Burp, Burping, Child, Female, Hiccup, Male, Man, Person, Woman"
|
||||||
|
HUMAN,COUGH,HMNCough,HMN,Human coughs and coughing.,"Adult, Ahem, Bronchitis, Child, Choking, Clear, Clearing, Convulse, Coughing, Covid, Expectoration, Female, Gag, Hack, Hacking, Hiccough, Hiccoughing, Hiccoughs, Hoarse, Male, Man, Person, Throat, Wet, Whooping, Woman"
|
||||||
|
HUMAN,HEARTBEAT,HMNHart,HMN,"Heartbeats, both literal as in a Dr. office, and stylized or designed heartbeats.","Adult, Arrhythmia, Arrhythmic, Arrythmia, Beating, Cardiac, Child, Coronary, EKG, Female, Fetal, Fibrillation, Heart, Male, Man, Palpitate, Palpitates, Palpitating, Palpitation, Palpitations, Person, Pulsate, Pulsated, Pulsates, Pulsating, Pulse, Rate, Rhythm, Woman"
|
||||||
|
HUMAN,KISS,HMNKiss,HMN,Human kissing.,"Adult, Child, Female, French, Hickey, Kiss, Lips, Make, Male, Man, Necking, Out, Peck, Person, Smooch, Smooching, Snog, Woman"
|
||||||
|
HUMAN,MISC,HMNMisc,HMN,Human sounds not fitting another category in this list.,"Adult, Child, Female, Male, Man, Miscellaneous, Person, Woman"
|
||||||
|
HUMAN,PEE,HMNPee,HMN,Human urinating.,"Adult, Child, Female, Male, Man, Micturition, Peeing, Person, Piss, Tinkle, Urinate, Urinating, Urination, Wee, Wizz, Woman"
|
||||||
|
HUMAN,SKIN,HMNSkin,HMN,"Human skin, Scratching, handshakes, skin on skin action, back rubbing, etc.","Adult, Backrub, Brush, Caress, Child, Clap, Dermis, Female, Flick, Grab, Hand, Handshake, Itch, Male, Man, Massage, Pat, Person, Pores, Rash, Rub, Rubbing, Scratch, Scratching, Skin, Slap, Tap, Woman"
|
||||||
|
HUMAN,SNEEZE,HMNSneez,HMN,Human sneezing.,"Achoo, Adult, Ah-Choo, Ahchoo, Allergic, Allergy, Child, Female, Fever, Hay, Male, Man, Nasal, Person, Sinus, Sneeze, Woman"
|
||||||
|
HUMAN,SNIFF,HMNSniff,HMN,"Human sniffing, smelling.","Adult, Child, Female, Inhalation, Inhale, Male, Man, Nose, Odor, Person, Smell, Sniff, Sniffing, Sniffle, Snivel, Snort, Snorting, Snuffle, Whiff, Woman"
|
||||||
|
HUMAN,SNORE,HMNSnor,HMN,Human snoring.,"Adult, Apnea, Breathing, Child, Congested, Doze, Dozing, Female, Heavy, Male, Man, Person, Slumber, Snooze, Snore, Snoring, Woman, ZZZ"
|
||||||
|
HUMAN,SPIT,HMNSpit,HMN,Human spits.,"Adult, Child, Drool, Expectoration, Female, Gob, Hawk, Hock, Male, Man, Mucus, Person, Phlegm, Saliva, Slobber, Spew, Spit, Spitoon, Spitting, Spittle, Spittoon, Sputum, Woman"
|
||||||
|
HUMAN,VOMIT,HMNVomit,HMN,Human vomiting.,"Adult, Barf, Barfing, Child, Chunder, Expunge, Female, Gag, Gagging, Heave, Hurl, Male, Man, Nausea, Nauseous, Person, Puke, Pukes, Reflex, Reflux, Regurgitate, Regurgitated, Regurgitation, Retch, Retched, Retching, Seasick, Spew, Throwing, Up, Upchuck, Vomit, Vomitus, Woman"
|
||||||
|
ICE,BREAK,ICEBrk,ICE,"Ice cracks, breaks, shatters. Any ice breaking that is fairly singular or isolated, large or small.","Apart, Break, Burst, Chip, Crack, Crumble, Crunch, Crush, Cube, Demolish, Destroy, Disintegrate, Fracture, Fragment, Ice, Iceberg, Icicle, Rips, Shatter, Smash, Snap, Splinter, Split"
|
||||||
|
ICE,CRASH & DEBRIS,ICECrsh,ICE,Ice large crashes and isolated debris go here. Iceberg calving.,"Apart, Break, Collision, Crash, Crevasse, Crush, Cube, Debris, Destroy, Fall, Fragments, Ice, Iceberg, Icicle, Pulverize, Remains, Rubble, Shards, Shatter, Smash, Wreck"
|
||||||
|
ICE,FRICTION,ICEFric,ICE,"Dry ice may go here, but often it's a metallic sound too, but ice scrapes, squeaks, creaks.","Abrade, Abrasion, Buff, Creak, Cube, Grind, Grinding, Hone, Ice, Icicle, Polish, Rasp, Rasping, Rub, Rubbing, Sand, Scour, Scrape, Scraping, Scratching, Screech, Scuffing, Sliding, Squeak, Stress, Wear"
|
||||||
|
ICE,HANDLE,ICEHndl,ICE,"Ice handling, picking up ice. Some overlap with ICE-MOVEMENT possible.","Catch, Clasp, Clench, Clutch, Cube, Down, Embrace, Grab, Grasp, Grip, Handle, Hold, Ice, Icicle, Pickup, Pluck, Seize, Set, Take, Throw, Toss"
|
||||||
|
ICE,IMPACT,ICEImpt,ICE,"Pieces of ice dropping (without breaking). Ice hits, solid ice impacts that don't break.","Bang, Banging, Bash, Bump, Chop, Clang, Clap, Clink, Clunk, Collide, Colliding, Crash, Crashing, Cube, Drop, Hit, Hitting, Ice, Icicle, Impact, Impacting, Jolt, Knock, Pound, Ram, Slam, Slamming, Smack, Smacking, Smash, Strike, Striking, Thrust"
|
||||||
|
ICE,MISC,ICEMisc,ICE,Ice sounds not fitting another category in this list.,"Block, Cap, Chill, Crystal, Cube, Floe, Formation, Freeze, Frigid, Frost, Frostbite, Frosty, Frozen, Glacier, Ice, Icicle, Icy, Miscellaneous, Shard, Sheet, Solid, Wintry"
|
||||||
|
ICE,MOVEMENT,ICEMvmt,ICE,"Ice sliding or rattling, maybe an object sliding across ice. See also ICE-HANDLE.","Calve, Calving, Cube, Drag, Drift, Float, Floating, Floe, Flow, Glide, Ice, Icicle, Move, Pile, Rattle, Roll, Shake, Shear, Shearing, Skid, Slide, Slip, Slither"
|
||||||
|
ICE,TONAL,ICETonl,ICE,"Tonal ice sounds, probably fairly rare, but that ice ping on a frozen lake would be one example.","Bowed, Cube, Frequency, Harmonic, Ice, Icicle, Melodic, Melodious, Musical, Ping, Pitch, Resonance, Resonant, Ring, Shing, Sonorous, Sound, Timbre, Tonal, Tone"
|
||||||
|
LASERS,BEAM,LASRBeam,LASR,"Steady laser beams, not a laser gun. So a James Bond style laser, a scientific laser.","Beam, Blast, Bolt, Emission, Energy, Flash, Gamma, Glare, Gleam, Laser, Lidar, Light, Maser, Projection, Pulse, Radiation, Ray, Shine, Spark, Stream"
|
||||||
|
LASERS,GUN,LASRGun,LASR,"Classic laser guns, blasters, laser cannons. That thing that Ben Burtt made famous.","Beam, Blaster, Cannon, Phaser, Plasma, Ray, Weapon"
|
||||||
|
LASERS,IMPACT,LASRImpt,LASR,"Classic laser impact sounds, from blaster hits.","Beam, Blast, Blaster, Burst, Explode, Hit, Impact, Ray, Sizzle, Strike"
|
||||||
|
LASERS,MISC,LASRMisc,LASR,Laser sounds not fitting another category in this list.,"Beam, Miscellaneous, Ray"
|
||||||
|
LEATHER,CREAK,LETHRCreak,LETHR,"Leather creaks, jacket and bag creaks, saddle movements. Any creaking leather.","Cowhide, Creak, Deerskin, Groan, Hide, Nubuck, Pigskin, Saddle, Sheepskin, Squeak, Stress, Stretch, Suede, Tension"
|
||||||
|
LEATHER,HANDLE,LETHRHndl,LETHR,"Leather handling, picking up a leather bag, putting on a leather jacket maybe. Some overlap with LEATHER-MOVEMENT possible.","Carry, Catch, Clasp, Clench, Clutch, Cowhide, Deerskin, Down, Embrace, Grab, Grasp, Grip, Handle, Hide, Hold, Nubuck, Pickup, Pigskin, Seize, Set, Sheepskin, Suede, Take, Throw, Toss, Wield"
|
||||||
|
LEATHER,IMPACT,LETHRImpt,LETHR,"Hits and impacts of leather, leather bag or saddle dropping.","Bang, Banging, Bump, Clap, Collide, Colliding, Cowhide, Crash, Crashing, Deerskin, Grab, Hide, Hit, Hitting, Impact, Impacting, Nubuck, Pigskin, Pound, Punch, Ram, Sheepskin, Slam, Slamming, Smack, Smacking, Strike, Striking, Suede, Thrust, Thud, Thump, Whack"
|
||||||
|
LEATHER,MISC,LETHRMisc,LETHR,Leather sounds not fitting another category in this list.,"Animal, Cowhide, Deerskin, Goatskin, Hide, Lambskin, Leather, Miscellaneous, Nubuck, Pigskin, Rawhide, Sheepskin, Skin, Suede"
|
||||||
|
LEATHER,MOVEMENT,LETHRMvmt,LETHR,"Movement of Leather, definitely overlap with LEATHER-CREAK and LEATHER-HANDLE.","Bend, Cowhide, Deerskin, Flap, Flex, Flop, Hide, Maneuver, Movement, Nubuck, Pigskin, Rotate, Rustle, Sheepskin, Slide, Stretch, Suede, Sway, Swing, Turn, Twist"
|
||||||
|
LIQUID & MUD,BUBBLES,LIQBubl,LIQ,"Mud and thicker than water, thick bubbles, viscous liquid. Things like Oil, gloopy thick liquids.","Boiling, Bubble, Bubbles, Burbling, Carbonation, Cavitation, Effervesce, Effervescence, Effervescent, Fizz, Foam, Froth, Frothy, Lather, Mud, Pop, Sparkle, Suds"
|
||||||
|
LIQUID & MUD,IMPACT,LIQImpt,LIQ,"Mud and thicker than water, Splats and hit. Things like Oil, gloopy thick liquids.","Hit, Impact, Kerplunk, Mud, Plop, Plunk, Slap, Slosh, Smack, Spatter, Splat, Splatter, Squelch, Squish"
|
||||||
|
LIQUID & MUD,MISC,LIQMisc,LIQ,Mud and thicker than water sounds not fitting another category in this list.,"Caramel, Flow, Gel, Glue, Gravy, Honey, Miscellaneous, Movement, Oil, Paint, Resin, Slime, Sludge, Syrup, Tar, Viscosity"
|
||||||
|
LIQUID & MUD,MOVEMENT,LIQMvmt,LIQ,"Mud and thicker than water, oozing, slime, glooping. Things like Oil, gloopy thick liquids.","Flooding, Goo, Gushing, Leak, Mud, Ooze, Rushing, Seep, Slime, Sloshing, Splashing, Surging, Swirling"
|
||||||
|
LIQUID & MUD,SUCTION,LIQSuct,LIQ,"Thicker than water, disgusting liquidy suction, plunger sucks. Things like Oil, gloopy thick liquids.","Draw, Glomp, Mud, Plunger, Pull, Slurp, Suck, Suction, Vacuum"
|
||||||
|
MACHINES,AMUSEMENT,MACHAmus,MACH,Theme park rides such as rollercoasters and carnival rides. Carnival game machines. General ambiences to go in AMBIENCE-AMUSEMENT.,"Apparatus, Bumper, Carousel, Carrousel, Cars, Coaster, Contraption, Device, Drop, Ferris, Flume, Freefall, Funhouse, Haunted, House, Kiddy, Line, Log, Machinery, Merry-Go-Round, Nickelodeon, Park, Pinball, Ride, Roller, Rollercoaster, Scrambler, Skeeball, Theme, Thrill, Tilt-A-Whirl, Tower, Waterslide, Wheel, Zip"
|
||||||
|
MACHINES,ANTIQUE,MACHAntq,MACH,"Old mechanical sewing machines, ancient looms, olive presses, a bit ambiguous but a very old mechanical machine.","Abacus, Adding, Antique, Apparatus, Arcade, Automaton, Babbage, Butter, Cash, Churn, Classic, Contraption, Device, Edison, Enigma, Guttenberg, Hand-Cranked, Heritage, Historic, Linotype, Loom, Machine, Machinery, Mimeograph, Nostalgic, Old-Fashioned, Olive, Press, Printing, Register, Retro, Sewing, Vintage"
|
||||||
|
MACHINES,APPLIANCE,MACHAppl,MACH,"General appliances, refrigerator, stove, oven, vacuum cleaner... AC and fans have their own category.","Air, Apparatus, Blender, Bread, Can, Cleaner, Coffee, Conditioner, Contraption, Cooker, Dehumidifier, Device, Dishwasher, Disposal, Dryer, Electric, Fan, Food, Fridge, Garbage, Griddle, Hair, Heater, Hot, Humidifier, Ice, Iron, Kettle, Machine, Machinery, Maker, Microwave, Mixer, Opener, Oven, Plate, Processor, Refrigerator, Rice, Sewing, Slow, Stand, Toaster, Vacuum, Waffle, Washing"
|
||||||
|
MACHINES,CONSTRUCTION,MACHCnst,MACH,"A machine for construction that isn't a vehicle or a tool. For example a cement mixer, a woodchipper.","Apparatus, Cement, Chipper, Compactor, Concrete, Contraption, Crane, Device, Dozer, Driver, Generator, Jackhammer, Lift, Machinery, Mixer, Paver, Pile, Scissor, Steamroller, Trencher, Wood"
|
||||||
|
MACHINES,ELEVATOR,MACHElev,MACH,Elevator movement and doors would go here.,"Apparatus, Contraption, Device, Dumbwaiter, Dumbwaiters, Freight, Hoist, Lift, Lifts, Machinery, Passenger, Paternoster, Platform, Service, Stair, Turbolift"
|
||||||
|
MACHINES,ESCALATOR,MACHEscl,MACH,Escalators and moving walkways would go here.,"Apparatus, Contraption, Device, Machinery, Moving, Stairs, Travelator, Walkway, Walkways"
|
||||||
|
MACHINES,FAN,MACHFan,MACH,"Fans of all types, box fans, ceiling fans. For air conditioning use MACHINES-HVAC.","Air, Apparatus, Bladeless, Blower, Box, Ceiling, Circulator, Contraption, Device, Exhaust, Industrial, Machinery, Ventilation"
|
||||||
|
MACHINES,GARDEN,MACHGrdn,MACH,"Powered landscaping machines, lawnmowers, weed-whackers, edgers, hedge trimmers. See also TOOLS-GARDEN and OBJECTS-GARDEN.","Apparatus, Blower, Chainsaw, Contraption, Cultivator, Device, Eater, Edger, Hedge, Irrigation, Landscaping, Lawn, Lawnmower, Leaf, Machinery, Mower, Mulcher, Pole, Pressure, Saw, Tiller, Tillers, Trimmer, Washer, Weed, Whacker"
|
||||||
|
MACHINES,GYM,MACHGym,MACH,"Exercise bikes, weight machines, treadmill, all things fitness. See also OBJECTS-GYM","Apparatus, Bike, Bowflex, Cardio, Climber, Contraption, Device, Elliptical, Exercise, Fitness, Machine, Machinery, Nautilus, Nordictrack, Resistance, Rower, Rowing, Stair, Stationary, Stepper, Trampoline, Treadmill, Weight, Workout"
|
||||||
|
MACHINES,HITECH,MACHTech,MACH,"Think cool James Bond machines, big elaborate death traps, maybe Rube Goldberg type machines. See also SCIFI-MACHINE.","3D, 7, Apparatus, Arm, Bond, CNC, Contraption, Cutter, Device, Digital, Gadget, Handheld, Holographic, James, Laser, Machinery, Plasma, Printer, Quantum, Resin, Robotic, Spy"
|
||||||
|
MACHINES,HVAC,MACHHvac,MACH,"Furnaces, Air Conditioners. Fans go in MACHINES-FAN. A close up gas furnace flame would go to FIRE-GAS.","Air, Aircon, Apparatus, Baseboard, Boiler, Chiller, Climate, Conditioner, Conditioning, Contraption, Control, Cooler, Dehumidifier, Device, Ductless, Evaporative, Filtration, Furnace, Handlers, Heat, Heater, Heating, Humidifier, Humidifiers, Machinery, Mini-Split, Pump, Pumps, Purifier, Radiator, Systems, Thermostat, Thermostats, Ventilation"
|
||||||
|
MACHINES,INDUSTRIAL,MACHInd,MACH,"Big industrial presses, factory machines, industrial robots, any heavy manufacturing machines.","Apparatus, Assembly, Auto, Automation, Contraption, Conveyor, Cutting, Device, Die, Distillery, Duty, Factory, Form, Heavy, Hydraulic, Industrial, Injection, Line, Machine, Machinery, Manufacturing, Milling, Molding, Plant, Press, Printing, Production, Punch, Reactor, Robotics, Robots, Sawmill, Shearing, Smelter, Smeltery, Stamper, System"
|
||||||
|
MACHINES,MECHANISM,MACHMech,MACH,"Complex mechanical smaller machines and gadgets, puzzle boxes that open portals to hell. Usually smaller devices.","7, Apparatus, Bond, Box, Contraption, Device, Gadget, Gizmo, Goldberg, James, Machinery, Puzzle, Rube, Spy"
|
||||||
|
MACHINES,MEDICAL,MACHMed,MACH,"Machines typically found in a hospital, medical lab or Doctor's office.","Apparatus, Blood, Cat, Centrifuge, Concentrator, Contraption, CT, Defibrillator, Dental, Device, Dialysis, Drill, ECG, EKG, Glucometers, Heart, Infusion, Insulin, Lab, Life, Machine, Machinery, Machines, Monitor, MRI, Nebulizer, Oxygen, Pacemaker, Pet, Pressure, Pump, Pumps, Scan, Support, Test, Ultrasound, Ventilator, X-Ray"
|
||||||
|
MACHINES,MISC,MACHMisc,MACH,Machine sounds not fitting another category in this list. See also MOTOR category.,"Apparatus, Contraption, Device, Machinery, Miscellaneous"
|
||||||
|
MACHINES,OFFICE,MACHOffc,MACH,"Machines typically found in an office, such as printers and copy machines.","Apparatus, Binding, Business, Clock, Contraption, Copier, Copy, Cutter, Device, Dictaphone, Envelope, Facsimile, Fax, Inkjet, Interactive, Label, Laminator, Laser, Letter, Machine, Machinery, Maker, Mimeograph, Office, Opener, Paper, Photocopier, Plotter, Postal, Printer, Scale, Scanner, Shredder, Stenotype, Telefax, Time, Typewriter, Whiteboard, Xerox"
|
||||||
|
MACHINES,PUMP,MACHPump,MACH,"Air or water pumps, fish tank aquarium pumps, sump pumps. Air compressors would fall into this category.","Aerator, Air, Apparatus, Backflow, Blower, Boilers, Centrifugal, Compressor, Contraption, Device, Fuel, Gas, Heat, Impellers, Inflators, Injector, Injects, Jet, Machinery, Piston, Plunger, Pump, Pumper, Pumping, Septic, Siphon, Sprinkler, Submersible, Sump, Syringe, Upflow, Vacuum, Valves, Water, Well, Wells"
|
||||||
|
MAGIC,ANGELIC,MAGAngl,MAG,"Angelic, Holy, Benevolent magic. The opposite of evil. Think choral tones and healing magic.","Angelic, Appearance, Aura, Beatific, Blessed, Blessedness, Blessings, Bliss, Celestial, Cherub, Creation, Divine, Ethereal, God, Guidance, Healing, Heavenly, Holy, Presence, Protection, Realm, Righteous, Sacred, Saint, Saintly, Seraph, Seraphic, Spiritual, Supernatural"
|
||||||
|
MAGIC,ELEMENTAL,MAGElem,MAG,"Magic centered around elements, fire, water, ice. Think the movie Frozen type stuff.","Air, Alchemic, Alchemical, Alchemy, Arcane, Earth, Electric, Energy, Fire, Fundamental, Natural, Nature, Planar, Primal, Primeval, Primordial, Spirits, Symbols, Transformation, Water, Wind"
|
||||||
|
MAGIC,EVIL,MAGEvil,MAG,"Evil magic, ominous, dark magic. Voldemort type stuff.","Bad, Bewitched, Black, Corrupt, Cruel, Dark, Demogorgon, Demonic, Demonifuge, Demonomagy, Depraved, Devil, Devilry, Deviltry, Diablerie, Diabolical, Diabolism, Fiendish, Forbidden, Hag, Hell, Hellbroth, Immoral, Infernal, Loki, Magic, Maleficent, Malevolent, Malicious, Malignant, Necromancy, Necronomicon, Nefarious, Nightmare, Occult, Pentacle, Satan, Satanic, Sinful, Sinister, Summon, Unholy, Vile, Villainous, Voodoo, Warlock, Wicked, Witchcraft"
|
||||||
|
MAGIC,MISC,MAGMisc,MAG,Magic sounds not fitting another category in this list.,"Alchemic, Alchemical, Alchemistic, Alchemy, Amulet, Artifact, Bewitchment, Ceremony, Charming, Conjurer, Conjuring, Elixir, Enchanting, Enchantments, Faerie, Fairie, Fairies, Fairylands, Fairytale, Incantational, Incantations, Magician, Magicians, Magick, Magickal, Magicks, Magique, Miracle, Miracles, Miraculous, Miscellaneous, Mystic, Mysticism, Mystique, Potion, Realm, Ritual, Sorcerous, Sorcery, Spellcraft, Spells, Symbolism, Trick, Trickery, Tricks, Wand, Wiz, Wizardly, Wizardry, Wizards, Wonder, Wondrous"
|
||||||
|
MAGIC,POOF,MAGPoof,MAG,"Shorter magic phrases, often for transforming, short 'turn you into a rabbit' type stuff.","Abracadabra, Alakazam, Appearance, Bibbidi-Bobbidi-Boo, Bim, Blast, Chuff, Disappearance, Foomp, Hocus-Pocus, Open, Poof, Presto, Sala, Sesame, Shazam, Sim, Ta-Da, Transformation, Transmutation, Vanish, Voila"
|
||||||
|
MAGIC,SHIMMER,MAGShim,MAG,"Glassy or chime-like shimmers, waves of particle type magic.","Aura, Bell, Chime, Gleam, Glimmer, Glimmering, Glint, Glisten, Glitter, Glittering, Gloss, Glow, Glowing, Incandescent, Light, Luminous, Luster, Radiant, Radiate, Reflection, Scintillate, Scintillation, Sheen, Shimmering, Shine, Shiny, Sparkle, Sparkling, Tree, Twinkle, Twinkling"
|
||||||
|
MAGIC,SPELL,MAGSpel,MAG,Something complex for a spell being cast. Think Maleficent swirling green magic or weird vocal-ish spells.,"Abracadabra, Banishing, Bewitch, Cast, Charm, Conjuration, Conjure, Conjured, Conjuring, Curse, Divination, Enchant, Enchanting, Enchantment, Enchantments, Evocation, Exorcism, Healing, Hex, Illusion, Illusionary, Illusionism, Illusionistic, Illusions, Incantation, Incantational, Incantations, Incantatory, Invocation, Invocations, Mesmerize, Prestidigitation, Protection, Rune, Sorcerer, Sorceries, Sorcerous, Sorcery, Spellacy, Spellbind, Spellbinding, Spellcheck, Spelled, Spelling, Spellings, Spells, Spellwork, Summoned, Summoning, Summons, Supernaturally, Trance, Voodoo, Warding, Witch, Wizard, Wizardly, Wizardry"
|
||||||
|
MECHANICAL,CLICK,MECHClik,MECH,"Short clicks, could be from button presses, things like that. Some confusion and overlap with MECHANICAL-SWITCH.","Button, Clack, Click, Clicker, Snap, Tick, Toggle"
|
||||||
|
MECHANICAL,GEARS,MECHGear,MECH,"A toothed cog, turning or grinding gears, complex gear mechanisms. Small clock gears would go in CLOCKS-MECHANICS.","Axle, Bevel, Cam, Chain, Cluster, Cog, Cogs, Cogwheels, Coupling, Crown, Derailleur, Differential, Drive, Flywheel, Flywheels, Gear, Gearbox, Geared, Gearing, Geartrain, Helical, Herringbone, Hypoid, Idler, Mesh, Meshing, Pinion, Pinions, Planetary, Rack, Reducer, Shaft, Shifter, Silent, Spiral, Sprocket, Sprockets, Spur, Straight-Cut, Synchromesh, Tooth, Transmission, Wheels, Worm"
|
||||||
|
MECHANICAL,HYDRAULIC & PNEUMATIC,MECHHydr,MECH,"Various hydraulic or pneumatic sounds, like on a self-closing door. Isolated hydraulics and pneumatics. See also DOORS-HYDRAULIC & PNEUMATIC for a complete door.","Actuator, Brake, Clutch, Compressor, Conveyor, Cylinder, Damper, Hydraulic, Jack, Pneumatic, Press, Pressurized, Ram, System, Valve"
|
||||||
|
MECHANICAL,LATCH,MECHLtch,MECH,"Latch, as in a briefcase latch, or a luggage latch.","Barrel, Bolt, Cabinet, Cam, Catch, Clasp, Deadlatch, Drawer, Fastener, Gate, Hardware, Hasp, Hook, Interlock, Latch, Lock, Magnetic, Night, Safety, Slam, Slide, Spring, Thumb, Toggle, Toolbox, Window"
|
||||||
|
MECHANICAL,LEVER,MECHLvr,MECH,"A sliding or switching lever. Train track lever, lever to turn on an electric chair. Old Frankenstein machine levers, breaker levers.","Brake, Cam, Clank, Clunk, Control, Joystick, Lever, Locking, Pedal, Push-Pull, Quick-Release, Rocker, Switch, Trigger"
|
||||||
|
MECHANICAL,LOCK,MECHLock,MECH,"A padlock, bicycle lock, or a locking mechanism. Door locks to go into DOORS-HARDWARE.","Biometric, Bolt, Cam, Catch, Clasp, Combination, Cylinder, Deadbolt, Electronic, Fastener, Hasp, Keyless, Latch, Lock, Mortise, Padlock, Pin, Security, Tumbler"
|
||||||
|
MECHANICAL,MISC,MECHMisc,MECH,Mechanical sounds not fitting another category in this list.,Miscellaneous
|
||||||
|
MECHANICAL,PULLEY,MECHPuly,MECH,"An isolated recording of a pulley, or system of pulleys.","And, Belt, Block, Chain, Chirp, Hoist, Pulley, Pulley-Block, Roll, Rope, Sheave, Squeak, Tackle, Winch"
|
||||||
|
MECHANICAL,RATCHET,MECHRtch,MECH,That sound a ratchet makes.,"Click, Crank, Detent, Pawl, Ratchet, Snap, Socket, Strap, Tie-Down, Winch, Wind, Wrench"
|
||||||
|
MECHANICAL,RELAY,MECHRelay,MECH,Old train station counting board flipping. A old communications relay board.,"Arc, Breaker, Clack, Coil, Contact, Control, Electromagnetic, Flip, Lamp, Solenoid, Switch, Toggle"
|
||||||
|
MECHANICAL,ROLLER,MECHRolr,MECH,"Like a rolling conveyor belt, or something on a roller.","Bearing, Belt, Dynamo, Line, Roll, Shaft"
|
||||||
|
MECHANICAL,SWITCH,MECHSwtch,MECH,"Basic and heavy switches, light switches, power switches. Some overlap or confusion with MECHANICAL-CLICK.","Button, Circuit, Clack, Click, Dimmer, Light, Limit, Micro, Push-Button, Rocker, Rotary, Safety, Selector, Slide, Switch, Switchgear, Toggle, Wall"
|
||||||
|
METAL,BREAK,METLBrk,METL,"Metal breaking, snaps, cracks, rips, shatters.","Aluminum, Apart, Beryllium, Brass, Break, Bronze, Burst, Bust, Cadmium, Chip, Chromium, Cleave, Cobalt, Copper, Crack, Crumble, Crunch, Crush, Demolish, Destroy, Disintegrate, Fracture, Fragment, Gallium, Gold, Indium, Iron, Lead, Lithium, Magnesium, Manganese, Mercury, Molybdenum, Nickel, Niobium, Palladium, Platinum, Rend, Rhenium, Rip, Rupture, Separate, Shatter, Silver, Smash, Snap, Splinter, Split, Steel, Tin, Titanium, Tungsten, Vanadium., Zinc"
|
||||||
|
METAL,CRASH & DEBRIS,METLCrsh,METL,"Metal crashing and debris. Complex metal crashes, group metal crashes, metal debris falls.","Aluminum, Beryllium, Brass, Bronze, Cadmium, Chromium, Clang, Clatter, Cobalt, Collision, Copper, Crash, Debris, Fall, Fragments, Gallium, Gold, Indium, Iron, Lead, Lithium, Magnesium, Manganese, Mercury, Molybdenum, Nickel, Niobium, Palladium, Platinum, Remains, Rhenium, Rubble, Ruins, Shards, Shrapnel, Silver, Smash, Steel, Tin, Titanium, Tungsten, Vanadium., Wreckage, Zinc"
|
||||||
|
METAL,FRICTION,METLFric,METL,"Metal friction, stress, so scrapes, creaks, squeaks, squeals, screeches. Metal sounds caused by friction.","Abrade, Abrasion, Aluminum, Beryllium, Brass, Bronze, Cadmium, Chafe, Chromium, Cobalt, Copper, Creak, Gallium, Gnash, Gold, Grind, Grinding, Indium, Iron, Lead, Lithium, Magnesium, Manganese, Mercury, Molybdenum, Nickel, Niobium, Palladium, Platinum, Rasp, Rasping, Rhenium, Rub, Rubbing, Scour, Scrape, Scraping, Scratching, Screech, Scuffing, Silver, Sliding, Squeak, Steel, Stress, Tin, Titanium, Tungsten, Vanadium., Wear, Zinc"
|
||||||
|
METAL,HANDLE,METLHndl,METL,"Metal handling, handling metal objects. Some overlap with METAL-MOVEMENT possible.","Aluminum, Beryllium, Brass, Bronze, Cadmium, Catch, Chromium, Clasp, Clench, Clutch, Cobalt, Copper, Down, Embrace, Gallium, Gold, Grab, Grasp, Grip, Handle, Handlebar, Hold, Indium, Iron, Knob, Latch, Lead, Lever, Lithium, Magnesium, Manganese, Mercury, Molybdenum, Nickel, Niobium, Operate, Palladium, Palm, Pickup, Platinum, Pull, Rhenium, Seize, Set, Silver, Steel, Take, Throw, Tin, Titanium, Toss, Tungsten, Use, Vanadium., Zinc"
|
||||||
|
METAL,IMPACT,METLImpt,METL,"Metal single item impacts, so anvil hits, dropping something that doesn't break. Clangs, clunks, bangs.","Aluminum, Bang, Banging, Bash, Beryllium, Brass, Bronze, Bump, Cadmium, Chromium, Clang, Clap, Clunk, Cobalt, Collide, Colliding, Copper, Crash, Crashing, Drop, Dropped, Gallium, Gold, Hit, Hitting, Impact, Impacting, Indium, Iron, Jolt, Knock, Lead, Lithium, Magnesium, Manganese, Mercury, Molybdenum, Nickel, Niobium, Palladium, Platinum, Pound, Ram, Rhenium, Ring, Silver, Slam, Slamming, Smack, Smacking, Smash, Steel, Strike, Striking, Thrust, Thump, Tin, Titanium, Tungsten, Vanadium., Zinc"
|
||||||
|
METAL,MISC,METLMisc,METL,Metal sounds not fitting another category in this list.,"Aluminum, Beryllium, Brass, Bronze, Cadmium, Chromium, Cobalt, Copper, Gallium, Gold, Indium, Iron, Lead, Lithium, Magnesium, Manganese, Mercury, Miscellaneous, Molybdenum, Nickel, Niobium, Palladium, Platinum, Rhenium, Silver, Steel, Tin, Titanium, Tungsten, Vanadium., Zinc"
|
||||||
|
METAL,MOVEMENT,METLMvmt,METL,"Metal movement, so drag, roll, rattle, shake. Single or metal items moving. Or movement on a metal surface. See also METAL-HANDLE.","Aluminum, Beryllium, Brass, Bronze, Cadmium, Chattering, Chromium, Clatter, Cobalt, Copper, Drag, Gallium, Glide, Gold, Indium, Iron, Jangle, Lead, Lithium, Magnesium, Manganese, Mercury, Molybdenum, Nickel, Niobium, Palladium, Platinum, Rattle, Rattling, Rhenium, Roll, Rolling, Scuffle, Shake, Shaking, Shift, Shuffle, Silver, Slide, Slip, Steel, Sway, Swing, Tin, Titanium, Tungsten, Vanadium., Vibrating, Wobble, Zinc"
|
||||||
|
METAL,TONAL,METLTonl,METL,"Metal tonal, resonating, ringing, wronks and warps. Bending sheet metal, a singing bowl ringing, metal shinging.","Aluminum, Beryllium, Bowed, Brass, Bronze, Cadmium, Chromium, Cobalt, Copper, Frequency, Gallium, Gold, Harmonic, Indium, Iron, Lead, Lithium, Magnesium, Manganese, Melodic, Melodious, Mercury, Molybdenum, Musical, Nickel, Niobium, Palladium, Ping, Pitch, Platinum, Resonance, Resonant, Rhenium, Ring, Shing, Silver, Sonorous, Sound, Steel, Timbre, Tin, Titanium, Tonal, Tone, Tungsten, Vanadium., Zinc"
|
||||||
|
MOTORS,ANTIQUE,MOTRAntq,MOTR,"Old motors, usually combustion, so old oil pump engines, old compressors. An antique gas pump for example.","Aged, Ancient, Antiquated, Bygone, Classic, Engine, Historic, Kerosene, Old-Fashioned, Paraffin, Retro, Steam, Traditional, Vintage, Windmill"
|
||||||
|
MOTORS,COMBUSTION,MOTRComb,MOTR,"Non-vehicle combustion engines. Combustion motors, generators. Isolated engine recordings.","Backfire, Carburation, Carburetor, Combustion, Combustor, Compression, Diesel, Engine, Ethanol, Four-Stroke, Gas, Gasoline, Generator, Internal, Manifold, Mechanic, Misfire, Motor, Outboard, Petrol, Rotary, Turboshaft, Two-Stroke"
|
||||||
|
MOTORS,ELECTRIC,MOTRElec,MOTR,Non-vehicle electric motors. Any motor that runs on electricity. An isolated electric motor. See also TOOLS-POWER.,"AC, DC, Dentist, Disposal, Drill, Dynamotor, Engine, Garbage, Magneto, Motor, Razor, Stepper, Toothbrush, Volt, Watt, Wheelchair"
|
||||||
|
MOTORS,MISC,MOTRMisc,MOTR,Motor sounds not fitting another category in this list.,"Engine, Miscellaneous"
|
||||||
|
MOTORS,SERVO,MOTRSrvo,MOTR,"Specifically servo motors, from robot, to radio controlled servos. Clean isolated servos.","3D, Actuator, Antenna, Camera, Car, Drive, Focus, Motor, Printer, RC, Robot"
|
||||||
|
MOTORS,TURBINE,MOTRTurb,MOTR,"Turbines, water or air. That classic 'whine' power up and down are usually turbines. A jet engine is a turbine.","Aircraft, Airplane, Engine, Generator, Hydro, Steam, Turbomachinery, Wind, Windmill"
|
||||||
|
MOVEMENT,ACTIVITY,MOVEActv,MOVE,"Crowds moving but also performing activities, cleaning, moving boxes. Clean recordings with no vocals. See MOVEMENT-HUMAN for Single activity.","Activity, Cleaning, Moving, Shopping, Unpacking"
|
||||||
|
MOVEMENT,ANIMAL,MOVEAnml,MOVE,"Group animal movement, individual feet go to FOOTSTEPS-ANIMAL or FOOTSTEPS-HORSE. But think herds of grazing animals, stampedes.","Charge, Flock, Herd, Migrate, Migration, Pack, Scramble, Stampede, Swarm, Trample"
|
||||||
|
MOVEMENT,CREATURE,MOVECrea,MOVE,"Slithering Dragons, Gooey Zombie Movement.","Burrowing, Coil, Contort, Crawl, Crawling, Digging, Lunge, Slide, Slither, Squirm, Wriggle, Writhe"
|
||||||
|
MOVEMENT,CROWD,MOVECrwd,MOVE,Groups of people milling or walking. Usually mostly just recordings of group footsteps in real locations. No vocals. Compare to MOVEMENT-ACTIVITY.,"Amble, Jump, Lie, March, Mill, Mobilize, Mobilizing, Moves, Pilgrimage, Rout, Run, Scuffle, Shuffle, Sit, Skip, Slip, Stand, Swarm, Trek, Troop, Walk"
|
||||||
|
MOVEMENT,HUMAN,MOVEHmn,MOVE,"Movement of single human, aside from footsteps, standing, sitting, shuffe. Some overlap or confusion with FOOTSTEP-HUMAN or MOVEMENT-ACTIVITY.","Activity, Climb, Crawl, Dance, Exercise, Lie, Mill, Perform, Physical, Scuffle, Shuffle, Sit, Slip, Stand"
|
||||||
|
MOVEMENT,INSECT,MOVEInsc,MOVE,"Group insects, footsteps, movement, swarms, spiders, locusts. Swarms of flying insects here too, bees, flies. Overlap with ANIMALS-INSECT or AMBIENCE-INSECT.","Ants, Beehive, Centipede, Crawl, Crawling, Earwig, Hive, Maggot, Maggots, Midge, Millipede, Mite, Pupa, Roach, Skittering, Slither, Spider, Squirm, Swarm, Tsetse, Writhe"
|
||||||
|
MOVEMENT,MISC,MOVEMisc,MOVE,Movement sounds not fitting another category in this list.,"Miscellaneous, Moves"
|
||||||
|
MOVEMENT,PRESENCE,MOVEPres,MOVE,"Very light movement, people in church adjusting, people on church pews. Again, recordings without vocals. Compare to MOVEMENT-ACTIVITY and MOVEMENT-CROWD.","Adjust, Fidget, Light, Milling, Movement, Restless, Scuffle, Settle, Shiffling, Shift, Shuffle"
|
||||||
|
MUSICAL,BELLS,MUSCBell,MUSC,"Specifically musical bells, tuned bells, think Tubular bells or hand bells. See also the BELL category for non-musical uses. BELL-GONG is older and this is a better fit.","Agogo, Agung, Babendil, Belldom, Bells, Bianzhong, Bonsho, Bourdon, Bowl, Campanology, Carillon, Cowbell, Gamelan, Gong, Hand, Handbells, Nola, Peal, Ring, Singing, Tam-Tam, Tintinnabulum, Tubular"
|
||||||
|
MUSICAL,BRASS,MUSCBrass,MUSC,"Brass musical instruments, trumpets, tuba, french horn.","Brassy, Bugle, Cornet, Euphonium, Euphorium, Flugelhorn, French, Horn, Mellophone, Orchestra, Sax, Saxhorn, Sousaphone, Trombone, Trumpet, Tuba"
|
||||||
|
MUSICAL,CHIME,MUSCChim,MUSC,"Wind chimes, bell trees, Tinkerbell type magic chimes.","Bell, Chime, Chimes, Chiming, Jingle, Shimmer, Tinkle, Tintinnabulation, Tree, Twinkle, Wind"
|
||||||
|
MUSICAL,CHORAL,MUSCChor,MUSC,"Choir pieces, choral, not for 'designed type vocally stuff'.","Acappella, Aria, Cantata, Cantor, Cantus, Choir, Choirmaster, Choral, Chorale, Chorus, Church, Ensemble, Glee, Gospel, Harmony, Hymn, Madrigal, Motet, Singer, Singers, Singing, Solo, Soprano, Tenor, Unison, Vocal, Vocalist, Vocals"
|
||||||
|
MUSICAL,EXPERIMENTAL,MUSCExpr,MUSC,"Strange or wierdly modified instruments, exotic instruments, prepared instruments like a prepared piano.","Abstract, Aeolian, Armonica, Artsy, Avant-Garde, Baschet, Cactus, Cristal, Electrified, Experimental, Exploratory, Glass, Harp, Innovative, Music, Pyrophone, Radical, Stylophone, Unconventional, Wind"
|
||||||
|
MUSICAL,INSTRUMENT,MUSCInst,MUSC,Various instruments that don't have a home in the other MUSICAL categories.,Instrument
|
||||||
|
MUSICAL,KEYED,MUSCKeyd,MUSC,"Keyed Instruments, such as pianos and harpsichord. For synthesizer though see MUSICAL-SYNTHESIZED.","Accordion, Celesta, Clavichord, Clavinet, Dulcitone, Electric, Fortepiano, Harmonium, Harpsichord, Keyboard, Melodica, Organ, Organetto, Piano, Pianoforte, Regal, Rhodes, Spinet, Virginal, Wurlitzer"
|
||||||
|
MUSICAL,LOOP,MUSCLoop,MUSC,"For looping musical phrases, sampled loops.","Drum, Loop, Looped, Loops, Music, Repeat, Repetition, Sample, Sampled, Sounds"
|
||||||
|
MUSICAL,MISC,MUSCMisc,MUSC,Music sounds not fitting another category in this list. Hopefully rarely needed.,"Harmonica, Kazoo, Miscellaneous"
|
||||||
|
MUSICAL,PERCUSSION,MUSCPerc,MUSC,"Drums and percussion, including deep timpani or big war drums. Mainly meant for true membrane instruments, also see MUSICAL-TUNED PERCUSSION","Bass, Beat, Block, Bongo, Cajon, Claves, Conga, Drum, Drumhead, Drumming, Drums, Frame, Kettle, Kettledrum, Kick, Ocean, Percussion, Percussionist, Rhythm, Set, Snare, Taiko, Timpani, Tom, Toms, Tympani, War, Wood"
|
||||||
|
MUSICAL,PERCUSSION TUNED,MUSCTnprc,MUSC,"Tuned percussion mostly played with mallets, Xylophones, Vibraphones, etc.","Balaphone, Bell, Belldom, Bianzhong, Celesta, Crotales, Drum, Glass, Glockenspiel, Handpan, Hang, Harmonica, Kalimba, Lithophone, Lyre, Mallet, Marimba, Marimbaphone, Metallophone, Musical, Saw, Steel, Vibraphone, Xylophone"
|
||||||
|
MUSICAL,PERFORMANCE,MUSCPrfm,MUSC,"A musical performance, or warm-up, a live musical act, a street musican, high school band. If a mixed ambience, look to AMBIENCE categories, like PUBLIC PLACE.","Act, Auditions, Ballet, Band, Bandleader, Classical, Compositions, Concert, Concertos, Concerts, Duet, Event, Gig, Live, Musicals, Musician, Musicians, Nightclub, Orchestra, Orchestral, Performance, Performances, Performer, Performs, Philharmonic, Play, Production, Recital, Recitalist, Recitals, Show, Singers, Songwriting, Spectacle, Stage, Stageplay, Street, Symphonic, Symphony, Theatrically, Vocalists"
|
||||||
|
MUSICAL,PLUCKED,MUSCPluck,MUSC,"Guitars, Banjos, Harps, instruments that are mainly plucked when played. See also MUSICAL-STRINGED.","Acoustic, Autoharp, Balalaika, Bandore, Bandura, Banjo, Bass, Bouzouki, Cither, Dulcimer, Guitar, Guzheng, Harp, Harpist, Jaw, Kalimba, Komuz, Kora, Koto, Lute, Lyre, Mandolin, Oud, Piano, Pipa, Pizzicato, Plectrum, Psaltery, Qanun, Rebab, Rota, Samisen, Saz, Shamisen, Sitar, Tambura, Tanbur, Thumb, Twang, Uke, Ukulele, Zither"
|
||||||
|
MUSICAL,SAMPLE,MUSCSmpl,MUSC,"Musical samples. Single triggered samples, not loops, which has its own subcategory here.","Audio, Bit, Bite, Clip, Excerpt, Fragment, One-Shot, Portion, Raw, Samples, Section, Slice, Snippet, Sound"
|
||||||
|
MUSICAL,SHAKEN,MUSCShake,MUSC,"Shaken musical instruments, rattles, tamourines.","Cabasa, Castanets, Chajchas, Egg, Gourd, Guiro, Katsa, Maraca, Maracas, Monkey, Rain, Rainstick, Rattles, Seed, Shaker, Shekere, Sistrum, Stick, Tambourine"
|
||||||
|
MUSICAL,SONG & PHRASE,MUSCSong,MUSC,"Complete musical pieces, songs as ringtones, car music bys. See also MUSICAL-PERFORMANCE which is a better fit for 'live performances'.","Anthem, Aria, Arrangement, By, Car, Chorus, Composition, Harmony, Lyric, Lyrical, Melody, Music, Musical, Part, Passage, Phrase, Piano, Player, Refrain, Riff, Ringtone, Segment, Singing, Song, Strain, Tune, Verse"
|
||||||
|
MUSICAL,STINGER,MUSCStngr,MUSC,"A short musical phrase or accent, like a horror 'string' sting. A big pitching cello note. But musical in nature.","Accent, Fanfare, Flourish, Interjection, Jingle, Music, News, Punctuation, Stab, Sting, Title"
|
||||||
|
MUSICAL,STRINGED,MUSCStr,MUSC,"Bowed string instruments like violin, cello, viola. Harp probably now goes to MUSICAL-PLUCKED.","Adagio, Bass, Bowed, Cello, Dahu, Double, Erhu, Fiddle, Hardanger, Hurdy-Gurdy, Nyckelharpa, Rebec, Viola, Violin, Violoncello"
|
||||||
|
MUSICAL,SYNTHESIZED,MUSCSynth,MUSC,"Synth samples, musical synth performances. Some overlap with MUSICAL-SAMPLE, but this meant for more than simple triggered samples.","Analog, ARP, Casio, Controller, Digital, Drum, Electronic, Keyboard, Korg, Machine, MIDI, Minimoog, Modular, Moog, Oberheim, Roland, Sequencer, Synth, Synthesizer, Theremin, Virtual, Vocoder, Yamaha"
|
||||||
|
MUSICAL,TOY,MUSCToy,MUSC,"Toys that play musical phrases, also music boxes would go here.","Box, Jack-in-the-box, Music, Musicbox"
|
||||||
|
MUSICAL,WOODWIND,MUSCWind,MUSC,"Woodwind instruments like flute, oboe, bassoon, clarinet.","Bagpipe, Bansuri, Bass, Basset, Bassoon, Clarinet, Contrabassoon, Cornett, Duduk, English, Fife, Flageolet, Flute, Hautboy, Horn, Kaval, Krumhorn, Ney, Oboe, Pan, Piccolo, Quena, Recorder, Sarrusophone, Saxophone, Shakuhachi, Suona, Tarogato, Xaphoon, Xiao"
|
||||||
|
NATURAL DISASTER,AVALANCHE,NATDAval,NATD,"Composite big movements of sliding, falling earth, landslides, mudslides. This category often for big hollywood versions of these types of sound.","Cascades, Crevasse, Earth, Earthslide, Icefall, Icefalls, Landslide, Landslides, Landsliding, Landslip, Lavafall, Mudflow, Mudslide, Rockfall, Rockfalls, Rockslide, Rockslides, Slide, Snow"
|
||||||
|
NATURAL DISASTER,EARTHQUAKE,NATDQuak,NATD,Composite and complex earthquake rumbles and rattles. This category often for big hollywood versions of these types of sound.,"Activity, Aftershock, Aftershocks, Disasters, Earth, Epicenter, Event, Fissure, Foreshock, Foreshocks, Ground, Hypocenter, Hypocentre, LFE, Quake, Quakes, Rattle, Rumble, Seism, Seismic, Seismism, Seismogram, Seismograms, Seismograph, Seismographic, Seismographs, Seismological, Seismologist, Seismologists, Seismology, Seismometer, Shake, Shakes, Shock, Tectonic, Temblor, Temblors, Tremor, Tremoring, Tremors"
|
||||||
|
NATURAL DISASTER,MISC,NATDMisc,NATD,Natural Disaster sounds not fitting another category in this list.,"Act, Apocalypse, Armageddon, Calamity, Cataclysm, Catastrophe, Disaster, Doomsdate, Doomsday, Drought, Famine, Flood, Force, God, Majeure, Megastorm, Meltdown, Miscellaneous, Of, Superstorm"
|
||||||
|
NATURAL DISASTER,TORNADO,NATDTorn,NATD,"Composite tornado sounds, layered and created massive tornadoes. This category often for big hollywood versions of these types of sound.","Cloud, Cyclone, Devil, Downburst, Downbursts, Dust, Duststorm, Duststorms, Eyewall, Funnel, Microburst, Supercell, Twister, Twisters, Updraft, Vortex, Waterspout, Waterspouts, Whirlwind, Whirlwinds, Windstorm"
|
||||||
|
NATURAL DISASTER,TSUNAMI,NATDTsun,NATD,"Composite tidal wave or tsunami scenes, layered and created. This category often for big hollywood versions of these types of sound.","Catastrophic, Earthquake, Flood, Floods, Giant, Mega, Megatsunami, Sea, Sunami, Supertide, Surge, Tidal, Tsunami, Tsunamigenic, Underwater, Wave"
|
||||||
|
NATURAL DISASTER,TYPHOON,NATDThyp,NATD,"Composite typhoons, cyclones or hurricanes. Massive swirling storms. This category often for big hollywood versions of these types of sound.","Cyclone, Cyclones, Cyclonic, Eyewall, Flood, Floods, Hurricane, Hurricanes, Monsoon, Monsoonal, Monsoons, Severe, Storm, Storm., Storms, Strong, Surge, Tempest, Tropical, Tyfoon, Typhon, Wind, Windstorm"
|
||||||
|
NATURAL DISASTER,VOLCANO,NATDVolc,NATD,"Composite big volcano eruptions. Lava, lava bombs, eruptions. This category often for big hollywood versions of these types of sound. See also GEOTHERMAL-LAVA.","Ash, Bombs, Caldera, Calderas, Chasma, Cloud, Crater, Cryovolcano, Erupting, Eruption, Eruptions, Eruptive, Flow, Fumarole, Geothermic, Kilauea, Lahar, Lava, Lavas, Magma, Magmasphere, Magmatic, Mantle, Pinatubo, Pyroclastic, Pyroclastics, Pyroclasts, Rhyolitic, Stratovolcano, Strombolian, Subvolcanic, Tephra, Volcanic, Volcanically, Volcanics, Volcanism, Volcanogenic, Volcanological, Volcanologist, Volcanologists, Volcanology, Vulcan, Vulcanian, Vulcanic, Vulcanism, Vulcano, Vulcanoid, Vulcanology"
|
||||||
|
OBJECTS,BAG,OBJBag,OBJ,"Bags and purses, shopping bags, lunch bags. Really any bag, except Luggage which would go to OBJECTS-LUGGAGE.","Back, Backpack, Briefcase, Camera, Carryall, Clutch, Courier, Drawstring, Duffel, Fanny, Handbag, Knapsack, Messenger, Pack, Pocketbook, Portfolio, Pouch, Purse, Rucksack, Sac, Sachet, Sack, Saddlebag, Satchel, Schoolbag, Shoulder, Sling, Tote"
|
||||||
|
OBJECTS,BOOK,OBJBook,OBJ,"Books, magazines, sounds relating to these. Handling particularly. Some overlap or confusion with PAPER-HANDLE.","Autobiography, Bible, Biography, Book, Bookkeeper, Bookkeeping, Booklet, Bookshop, Bookstore, Brochure, Catalog, Catalogue, Chapter, Codex, Compendium, Compilation, Cookbook, Cover, Daybook, Diary, Dictionary, Directory, Edition, Encyclopedia, Fiction, Guidebook, Handbook, Hardcover, Inventory, Journal, Ledger, Leger, Letters, Literary, Literature, Magazine, Manuscript, Memoir, Non-Fiction, Notebook, Novel, Page, Pamphlet, Paperback, Portfolio, Publication, Publishing, Read, Reading, Scrapbook, Script, Scroll, Sketchbook, Story, Synopsis, Textbook, Tome, Volume, Workbook, Works, Worksheet, Writer, Yearbook"
|
||||||
|
OBJECTS,COIN,OBJCoin,OBJ,"Coins land, toss, drop, handle. Coins being counted, sorted. A bag of coins.","Bank, Cent, Change, Clink, Coinage, Currency, Dime, Dollar, Doubloon, Dump, Euro, Flip, Half-Dollar, Loose, Money, Nickel, Penny, Piggy, Ping, Pound, Quarter, Rouble, Rupee, Shekel, Silver, Spin, Token, Toss, Yen"
|
||||||
|
OBJECTS,CONTAINER,OBJCont,OBJ,"Boxes, tupperware, jewelry boxes, things that hold things, lids, handling, opens and closes.","Aquarium, Baggie, Barrel, Basket, Bin, Box, Breadbox, Canister, Canisters, Capsule, Cardboard, Carton, Case, Casket, Chest, Cistern, Contain, Crate, Dispenser, Dumpster, Enclosure, Flask, Holder, Jar, Jars, Jewelry, Lid, Locker, Package, Pill, Plastic, Receptacle, Receptacles, Reliquary, Toolbox, Tray, Trunk, Tupperware, Vase, Vial"
|
||||||
|
OBJECTS,FASHION,OBJFash,OBJ,"Shoes, belts, clothes, actions involding fashion objects. Makeup, lipstick, etc. Some overlap possible with CLOTH categories. See also OBJECTS-JEWELRY. And OBJECTS-BAG.","Apparel, Attire, Bandana, Belt, Boots, Brush, Clothing, Clutch, Comb, Couture, Eyeliner, Garment, Glasses, Gloves, Handkerchief, Hat, Headband, High-Heels, Lipstick, Makeup, Mascara, Muffs, Outfit, Robe, Sash, Scarf, Shawl, Shoes, Sneakers, Socks, Sunglasses, Suspenders, Ties, Wallet, Wardrobe, Watches"
|
||||||
|
OBJECTS,FURNITURE,OBJFurn,OBJ,"Chairs movement, couch movement, things like that. Wood chair creaks could go in WOOD-FRICTION.","Antiques, Armoires, Bed, Bedframe, Bench, Bookcase, Bookcases, Bookshelf, Buffet, Cabinet, Cabinets, Chair, Chest, Coffee, Console, Couch, Couches, Credenza, Crib, Desk, Dining, Dresser, Dressers, End, Footlocker, Futon, Hutch, Loveseat, Mattress, Mattresses, Nightstand, Ottoman, Rocking, Rugs, Shelf, Shelving, Sideboard, Sofa, Sofas, Stool, Table, Upholstery, Wardrobe"
|
||||||
|
OBJECTS,GARDEN,OBJGrdn,OBJ,"Flower pots, bird feeders, things that you might find and use in a garden. See also TOOLS-GARDEN and MACHINES-GARDEN.","Bath, Bin, Bird, Can, Compost, Composter, Faucet, Feeder, Flower, Gloves, Greenhouse, Hose, Killer, Labels, Plant, Pot, Sculpture, Stake, Stakes, Trellis, Watering, Weed"
|
||||||
|
OBJECTS,GYM,OBJGym,OBJ,"Objects found in a fitness center or school gym, maybe a yoga mat, dumbells. See also MACHINES-GYM.","Ball, Band, Bar, Barbell, Bench, Boxing, Dumbbell, Exercise, Fitness, Foam, Gloves, Jump, Kettlebell, Mat, Medicine, Pull-Up, Rack, Resistance, Roller, Rope, Stepper, Weight, Yoga"
|
||||||
|
OBJECTS,HOUSEHOLD,OBJHsehld,OBJ,"Cleaning products, hangers, towel racks, things around the house that don't fit somewhere else.","Broom, Can, Cleaning, Domestic, Home, House, Mirror, Mop, Rug, Soap, Sponge, Supplies, Trash"
|
||||||
|
OBJECTS,JEWELRY,OBJJewl,OBJ,"Bracelets, necklaces, rings.","Accessories, Adornment, Adornments, Anklet, Bangles, Baubles, Bead, Beaded, Beads, Beadwork, Bling, Bracelet, Bracelets, Bridal, Brooch, Brooches, Cabochons, Charm, Choker, Crown, Cufflink, Cufflinks, Diamond, Diamonds, Earing, Earring, Earrings, Gem, Gemstone, Gemstones, Hairpin, Jewel, Jeweled, Jeweler, Jewelers, Jewelery, Jeweller, Jewellery, Jewels, Locket, Necklace, Necklaces, Nose, Ornament, Ornaments, Pendant, Pendants, Pin, Ring, Tiara, Tie, Trinkets, Watch"
|
||||||
|
OBJECTS,KEYS,OBJKey,OBJ,"Keys jingling, keys movement in lock.","Bicycle, Car, Card, Fob, House, Key, Keychain, Keyes, Keyholes, Keylock, Keyring, Keys, Keyset, Lanyard, Lock, Locker, Mailbox, Master, Office, Padlock, Safe, Skeleton, Unlock, Unlocking, Unlocks"
|
||||||
|
OBJECTS,LUGGAGE,OBJLug,OBJ,"Suitcases, luggage related sounds, handling, opening, packing.","Backpack, Bag, Baggage, Baggages, Bagroom, Bellhop, Carry-On, Carryon, Checked, Cubes, Duffel, Duffels, Duffle, Garment, Knapsack, Knapsacks, Luggage, Packing, Rolling, Rucksack, Samsonite, Satchel, Scale, Skycap, Stow, Stowing, Suitcase, Suitcases, Tag, Toiletry, Trolley, Trunks"
|
||||||
|
OBJECTS,MEDICAL,OBJMed,OBJ,Common objects in hospital or medical office.,"Aid, Bandage, Blood, Brace, Cast, Clamp, Crutches, Cuff, Epipen, Forceps, Gauss, Gauze, Gloves, Hearing, Mask, Meter, Pills, Pressure, Rubber, Scalpel, Specimen, Stethoscope, Surgical, Swab, Syringe, Thermometer"
|
||||||
|
OBJECTS,MISC,OBJMisc,OBJ,Object sounds not fitting another category in this list.,Miscellaneous
|
||||||
|
OBJECTS,OFFICE,OBJOffc,OBJ,"Things found in an office, staplers, paper cutter etc. Not machines like a printer which would go in MACHINES-OFFICE.","Binder, Business, Calculator, Calendar, Can, Card, Chair, Cutter, Desk, Envelope, Folder, Hole, Notepad, Office, Paper, Paperweight, Post-It, Punch, Report, Scissors, Stapler, Supplies, Trash, Tray, Whiteboard"
|
||||||
|
OBJECTS,PACKAGING,OBJPack,OBJ,"Items commonly used in packaging or packing, so bubblewrap, styrofoam, labels, food packaging.","Bag, Box, Bubble, Cardboard, Corrugated, Crate, Envelope, Foam, Label, Labeling, Labelling, Labels, Mailing, Overwrap, Package, Packagings, Packed, Packing, Padding, Pallet, Paper, Parcel, Peanuts, Shipment, Shipping, Shrink, Strapping, Stretch, Styrofoam, Unwrapping, Wrap, Wrapper, Wrappers, Wrapping, Wrappings"
|
||||||
|
OBJECTS,TAPE,OBJTape,OBJ,Tape and other tacky sticky type sounds as well. Velcro probably better into CLOTH-RIP as that matches the 'sound' better.,"Adhesive, Cellophane, Double, Double-Sided, Duct, Electrical, Flypaper, Gaffer, Glue, Masking, Medical, Packing, Scotch, Sellotape, Stick, Sticky, Transparent, Velcro"
|
||||||
|
OBJECTS,UMBRELLA,OBJUmbr,OBJ,"Umbrella opens and closes. Not for rain on umbrella, which would go in RAIN-CLOTH.","Beach, Brolly, Broly, Bumbershoot, Canopy, Compact, Gamp, Golf, Pagoda, Parasol, Patio, Rain, Sunshade, Travel, Umbrella"
|
||||||
|
OBJECTS,WHEELED,OBJWhled,OBJ,"Non-motorized wheeled carts and dollies. Shopping cart, food cart, baggage cart, baby stroller, dolly, wheelchairs.","Baby, Barrow, Caddie, Carriage, Cart, Caster, Dolly, Gurney, Hand, Handcart, Luggage, Perambulator, Pram, Pushcart, Rolled, Rolling, Shopping, Skateboard, Stroller, Suitcase, Truck, Wagon, Walker, Wheelbarrow, Wheelchair"
|
||||||
|
OBJECTS,WRITING,OBJWrite,OBJ,"Any writing object or performance of writing on any surface. Pens, pencils, crayons, markers.","Blotter, Blotting, Board, Calligraphy, Chalk, Charcoal, Crayon, Drafting, Drawing, Dry, Erase, Eraser, Fountain, Handwriting, Highlighter, Ink, Inscription, Lettering, Letters, Marker, Pen, Pencil, Penned, Penning, Print, Printed, Quill, Script, Sharpie, Signature, Spelled, Spelling, Writer, Written"
|
||||||
|
OBJECTS,ZIPPER,OBJZipr,OBJ,"Pant zippers, bag zippers, luggage zippers.","Bag, Closure, Closures, Fastener, Jacket, Pants, Slider, Tent, Zip, Zipper, Zips"
|
||||||
|
PAPER,FLUTTER,PAPRFltr,PAPR,"Paper and Cardboard. Tossing a group of pages or a book in the air, money flutters as it falls to the ground. Confetti.","Cardboard, Confetti, Crinkling, Fall, Flap, Flapping, Flittering, Flutter, Fluttering, Kraft, Manila, Paper, Papyrus, Parchment, Riffle, Ruffle, Rustle, Rustling, Shake, Tissue, Vellum, Waving"
|
||||||
|
PAPER,FRICTION,PAPRFric,PAPR,Paper and Cardboard. Sliding paper or scraping paper.,"Abrading, Cardboard, Count, Flip, Kraft, Manila, Paper, Papyrus, Parchment, Rub, Rubbing, Scraping, Scratch, Scratching, Scuffing, Sliding, Tissue, Vellum"
|
||||||
|
PAPER,HANDLE,PAPRHndl,PAPR,"Paper and Cardboard. Page turns, magazine handling. Books dropping. Handling paper money. Some overlap or confusion with OBJECTS-BOOK.","Bill, Browse, Cardboard, Clasp, Clutch, Count, Crease, Crinkle, Crumple, Flip, Fold, Grip, Handle, Holder, Kraft, Manila, Page, Paper, Papyrus, Parchment, Peruse, Pull, Roll, Sheet, Throw, Tissue, Toss, Turn, Vellum"
|
||||||
|
PAPER,IMPACT,PAPRImpt,PAPR,"Paper and Cardboard. Heavy paper hits, a rolled up newspaper thwack, a magazine hitting something.","Bump, Cardboard, Drop, Dropped, Hit, Kraft, Magazine, Manila, Newspaper, Paper, Papyrus, Parchment, Punch, Slam, Slap, Smack, Strike, Thump, Tissue, Vellum, Whack"
|
||||||
|
PAPER,MISC,PAPRMisc,PAPR,Paper and Cardboard. Paper sounds not fitting another category in this list.,"Carbon, Cardstock, Construction, Copy, Craft, Crepe, Deed, Document, Dossier, Kraft, Leaf, Letter, Manila, Manuscript, Miscellaneous, Newsprint, Note, Notebook, Page, Paper, Papyrus, Parchment, Printer, Rice, Scroll, Sheet, Stationery, Tissue, Tracing, Vellum, Writing"
|
||||||
|
PAPER,RIP,PAPRRip,PAPR,"Paper and Cardboard. Ripping paper, paper tears.","Cardboard, Kraft, Manila, Paper, Papyrus, Parchment, Rend, Scraps, Shred, Tatter, Tear, Tearing, Tears, Tissue, Tore, Vellum"
|
||||||
|
PAPER,TONAL,PAPRTonl,PAPR,"Paper and Cardboard. Not very common, but tonal paper sounds.","Bowed, Frequency, Harmonic, Kraft, Manila, Melodic, Melodious, Musical, Papyrus, Parchment, Ping, Pitch, Resonance, Resonant, Ring, Shing, Sonorous, Sound, Timbre, Tissue, Tonal, Tone, Vellum"
|
||||||
|
PLASTIC,BREAK,PLASBrk,PLAS,"Plastic breaking, crunches, rips, shatters.","ABS, Acetate, Acrylic, Break, Burst, Chloride, Crack, Crunch, Crush, Demolish, Destroy, Fracture, Fragment, HDPE, Nylon, Polycarbonate, Polypropylene, Polystyrene, Polyvinyl, PVC, Rip, Shatter, Smash, Snap, Splinter, Split"
|
||||||
|
PLASTIC,CRASH & DEBRIS,PLASCrsh,PLAS,"Plastic crashing, groups items falling, group destruction, plastic debris.","ABS, Acetate, Acrylic, Bits, Break, Chloride, Collision, Crash, Debris, Fall, Fragments, HDPE, Nylon, Pieces, Polycarbonate, Polypropylene, Polystyrene, Polyvinyl, PVC, Remains, Rubble, Ruins, Shards, Shatter, Smash, Wreckage"
|
||||||
|
PLASTIC,FRICTION,PLASFric,PLAS,"Plastic friction, such as scrapes, creaks, squeaks, screeching.","Abrasion, ABS, Acetate, Acrylic, Chloride, Creak, Creaks, Friction, Grate, Grind, Grinding, Groan, HDPE, Nylon, Polycarbonate, Polypropylene, Polystyrene, Polyvinyl, PVC, Rasp, Rasping, Rub, Rubbing, Scrape, Scraping, Scratch, Scratching, Screech, Scuffing, Sliding, Squeak, Squeaks, Stress, Wear"
|
||||||
|
PLASTIC,HANDLE,PLASHndl,PLAS,"Plastic handling, handling a toy or remote control. Some overlap with PLASTIC-MOVEMENT possible.","ABS, Acetate, Acrylic, Catch, Chloride, Clasp, Clench, Clutch, Down, Embrace, Grab, Grasp, Grip, Handle, HDPE, Hold, Nylon, Operate, Pickup, Pluck, Polycarbonate, Polypropylene, Polystyrene, Polyvinyl, PVC, Seize, Set, Snap, Take, Throw, Toss, Use"
|
||||||
|
PLASTIC,IMPACT,PLASImpt,PLAS,"Plastic item impact. Hit, drop, big plastic impacts where item does not break. Dropping a plastic object.","ABS, Acetate, Acrylic, Bang, Banging, Bash, Bump, Chloride, Clap, Collide, Colliding, Crash, Crashing, Drop, Dropped, HDPE, Hit, Hitting, Impact, Impacting, Jolt, Knock, Nylon, Polycarbonate, Polypropylene, Polystyrene, Polyvinyl, Pound, Punch, PVC, Ram, Slam, Slamming, Smack, Smacking, Strike, Striking, Thrust, Thump"
|
||||||
|
PLASTIC,MISC,PLASMisc,PLAS,Plastic sounds not fitting another category in this list.,"ABS, Acetate, Acrylic, Artificial, Chloride, Fiber, HDPE, Industrial, Man-Made, Manufactured, Miscellaneous, Non-Biodegradable, Nylon, Plasticine, Polycarbonate, Polyethylene, Polymer, Polypropylene, Polystyrene, Polyurethane, Polyvinyl, PTFE, PVA, PVC, Resin, Synthetic"
|
||||||
|
PLASTIC,MOVEMENT,PLASMvmt,PLAS,"Plastic movement, dragging plastic or drag on plastic, rattles, shakes. See also PLASTIC-HANDLE.","ABS, Acetate, Acrylic, Agitate, Bump, Chloride, Clatter, Drag, HDPE, Jangle, Jiggle, Jingle, Nylon, Polycarbonate, Polypropylene, Polystyrene, Polyvinyl, PVC, Rattle, Roll, Ruffle, Rustle, Shake, Stir, Sway, Vibrate, Wobble"
|
||||||
|
PLASTIC,TONAL,PLASTonl,PLAS,"Plastic tonal sounds, with a musical quality. Probably pretty rare.","ABS, Acetate, Acrylic, Bowed, Chloride, Frequency, Harmonic, HDPE, Melodic, Melodious, Musical, Nylon, Ping, Pitch, Polycarbonate, Polypropylene, Polystyrene, Polyvinyl, PVC, Resonance, Resonant, Ring, Shing, Sonorous, Sound, Timbre, Tonal, Tone"
|
||||||
|
RAIN,CLOTH,RAINClth,RAIN,"Rain on cloth, like a person's clothes, or more likely a patio umbrella, awning.","Awning, Cloth, Cotton, Drizzle, Jacket, Precipitation, Rain, Raincoat, Rainwear, Tarp, Tent, Umbrella, Waterproof"
|
||||||
|
RAIN,CONCRETE,RAINConc,RAIN,Rain on asphalt or concrete or patio tile. Rain on a hard slappy solid surface.,"Asphalt, Brick, Cement, Drizzle, Masonry, Path, Pavement, Precipitation, Rain, Sidewalk, Street"
|
||||||
|
RAIN,GENERAL,RAIN,RAIN,"General downpour, rain on mixed materials.","Damp, Deluge, Downpour, Drenched, Drizzle, Mist, Monsoon, Precipitate, Precipitation, Rain, Rainfall, Raining, Rains, Rainstorm, Rainy, Shower, Sprinkle, Torrent, Wet"
|
||||||
|
RAIN,GLASS,RAINGlas,RAIN,"Rain on a glass roof like a sunroof, or rain blown against a window as well.","Drizzle, Glass, Greenhouse, Pane, Precipitation, Rain, Sheet, Skylight, Sunroof, Window, Windshield"
|
||||||
|
RAIN,INTERIOR,RAINInt,RAIN,"Interior recordings of Rain, from inside house or barn, or vehicle.","Drizzle, Indoor, Interior, Precipitation, Rain, Rainstorm, Roof, Sunroof, Windshield"
|
||||||
|
RAIN,METAL,RAINMetl,RAIN,"Rain on a metal surface, so exterior rain on a car, or metal sheet, metal roof.","Can, Car, Drizzle, Gutters, Metal, Ping, Precipitation, Rain, Roof, Tin"
|
||||||
|
RAIN,PLASTIC,RAINPlas,RAIN,"Rain on plastic or vinyl surfaces, like a grill cover, or a plastic shed.","Drizzle, Plastic, Plexiglass, Precipitation, Rain, Vinyl"
|
||||||
|
RAIN,VEGETATION,RAINVege,RAIN,"Rain on plants, leaves and grass. Jungle or tropical rain on plant leaves.","Drizzle, Foliage, Grass, Jungle, Leaf, Leaves, Plants, Precipitation, Rainforest, Tree, Vegetation"
|
||||||
|
RAIN,WATER,RAINWatr,RAIN,"Rain on water, puddles, lakes, ponds.","Drizzle, Droplets, Drops, Lake, Pond, Precipitation, Puddle, Rain, Ripples, Surface"
|
||||||
|
RAIN,WOOD,RAINWood,RAIN,"Rain on wood surfaces, so a deck for example.","Barn, Deck, Drizzle, Floor, Precipitation, Rain, Roof, Shed, Wood"
|
||||||
|
ROBOTS,MISC,ROBTMisc,ROBT,Robot sounds not fitting another category in this list.,"AI, Android, Androids, Artificial, Automatons, Beings, Bionic, Bot, Cybernetic, Cyborg, Cyborgs, Droid, Droids, Humanoid, Lifeforms, Machines, Mechs, Synthetic"
|
||||||
|
ROBOTS,MOVEMENT,ROBTMvmt,ROBT,"Complex robot movement, servos, possible robot hovering and flying. See also MOTORS-SERVO.","Actuator, AI, Android, Androids, Automatons, Bionic, Bot, Cybernetic, Cyborg, Cyborgs, Droid, Droids, Hydraulic, Machines, Mechanism, Mechs, Pneumatic, Servo, Solenoid, Synthetic"
|
||||||
|
ROBOTS,VOCAL,ROBTVox,ROBT,"Droid and robot vocals, R2D2 would go here. Any manner of vocalizing robot. More expressive than a beep/UI. Robot language.","AI, Android, Androids, Artificial, Automatons, Beep, Beings, Bionic, Bot, Chirp, Cybernetic, Cyborg, Cyborgs, Droid, Droids, Lifeforms, Machines, Mechs, R2D2, Synthetic"
|
||||||
|
ROCKS,BREAK,ROCKBrk,ROCK,"Rock cracks, breaks, rips or shatters. More isolated and singular cracks and breaks.","Apart, Basalt, Boulders, Break, Breaks, Burst, Chip, Cobblestones, Crack, Cracks, Crumble, Crunches, Crush, Demolish, Destroy, Disintegrate, Formations, Fossil, Fracture, Fragment, Geology, Gneiss, Granite, Gravel, Gravelly, Hammered, Hammering, Hammers, Igneous, Limestone, Marble, Metamorphic, Minerals, Obsidian, Pebbles, Pickaxe, Pumice, Quarry, Quartzite, Rips, Sandstone, Schist, Scree, Sedimentary, Shale, Shatter, Shatters, Shingles, Slab, Slate, Slates, Smash, Snap, Specimens, Splinter, Split, Stones"
|
||||||
|
ROCKS,CRASH & DEBRIS,ROCKCrsh,ROCK,"Rock crashes and debris, so a huge rock crash, collapse. Or isolated debris. See also NATURAL DISASTER-AVALANCHE.","Apart, Avalanche, Basalt, Boulders, Break, Clastic, Cobblestones, Collision, Crack, Crumble, Debris, Disintegrate, Down, Fall, Formations, Fracture, Fragment, Fragments, Geology, Gneiss, Granite, Gravel, Gravelly, Gritty, Igneous, Limestone, Marble, Metamorphic, Minerals, Moraine, Obsidian, Ore, Pebbles, Pumice, Quartzite, Remains, Rockfall, Rockslide, Rocky, Rubble, Ruins, Sandstone, Schist, Scree, Sedimentary, Shale, Shards, Shatter, Shingles, Slab, Slate, Slates, Smash, Specimens, Splinter, Stones, Up, Wreckage"
|
||||||
|
ROCKS,FRICTION,ROCKFric,ROCK,"Rock creaks, scrapes... the stress of rock about to break.","Abrade, Abrasion, Basalt, Boulders, Cobblestones, Creaks, Erode, Formations, Geology, Gneiss, Granite, Grate, Gravel, Gravelly, Grind, Grinding, Igneous, Limestone, Marble, Metamorphic, Minerals, Obsidian, Pebbles, Pumice, Quartzite, Rasp, Rasping, Rub, Rubbing, Sandstone, Schist, Scour, Scrape, Scrapes, Scraping, Scratch, Scratching, Scree, Screech, Scuffing, Sedimentary, Shale, Shingles, Slab, Slate, Slates, Sliding, Specimens, Squeaks, Stones, Stress, Wear, Whet, Whetstone"
|
||||||
|
ROCKS,HANDLE,ROCKHndl,ROCK,"Rock handing, picking up or digging through rocks, bricks. Some overlap with ROCKS-MOVEMENT possible.","Basalt, Boulders, Carry, Catch, Clasp, Clench, Clutch, Cobblestones, Down, Embrace, Formations, Geology, Gneiss, Grab, Granite, Grasp, Gravel, Gravelly, Grip, Handle, Hold, Igneous, Lift, Limestone, Manipulate, Marble, Metamorphic, Minerals, Move, Obsidian, Pebbles, Pickup, Pumice, Quartzite, Sandstone, Schist, Scree, Sedimentary, Seize, Set, Shale, Shingles, Slab, Slate, Slates, Specimens, Stones, Take, Throw, Toss"
|
||||||
|
ROCKS,IMPACT,ROCKImpt,ROCK,"Rock falling, dropping without breaking, or something hit with a rock, a big rock impact.","Bang, Banging, Basalt, Bash, Boulders, Bump, Chop, Clang, Clap, Clink, Clunk, Cobblestones, Collide, Colliding, Collision, Crash, Crashing, Drop, Dropped, Formations, Geology, Gneiss, Granite, Gravel, Gravelly, Hit, Hitting, Igneous, Impact, Impacting, Jolt, Knock, Limestone, Marble, Metamorphic, Minerals, Obsidian, Pebbles, Pound, Pumice, Quartzite, Ram, Sandstone, Schist, Scree, Sedimentary, Shale, Shingles, Shock, Slab, Slam, Slamming, Slate, Slates, Smack, Smacking, Specimens, Stones, Strike, Striking, Thrust, Thud"
|
||||||
|
ROCKS,MISC,ROCKMisc,ROCK,Rock sounds not fitting another category in this list.,"Basalt, Boulders, Cobblestones, Formations, Geology, Gneiss, Granite, Gravel, Gravelly, Igneous, Limestone, Marble, Metamorphic, Minerals, Miscellaneous, Obsidian, Pebbles, Pumice, Quartzite, Sandstone, Schist, Scree, Sedimentary, Shale, Shingles, Slab, Slate, Slates, Specimens, Stones"
|
||||||
|
ROCKS,MOVEMENT,ROCKMvmt,ROCK,"Rock dragging, or dragging across a rock surface, rocks shaking or rattling. See also ROCKS-HANDLE.","Agitate, Basalt, Boulders, Cobblestones, Displace, Drag, Fall, Formations, Geology, Glide, Gneiss, Granite, Gravel, Gravelly, Igneous, Jiggle, Jolt, Limestone, Marble, Metamorphic, Minerals, Move, Obsidian, Oscillate, Pebbles, Pumice, Quake, Quartzite, Rattle, Rockslide, Roll, Sandstone, Schist, Scree, Sedimentary, Shake, Shale, Shift, Shingles, Shudder, Slab, Slate, Slates, Slide, Slip, Slipping, Slips, Specimens, Stones, Sway, Swing, Teeter, Topple, Tremble, Tumble, Vibrate, Wobble"
|
||||||
|
ROCKS,TONAL,ROCKTonl,ROCK,"Tonal rocks, probably pretty rare. Singing rocks.","Basalt, Boulders, Bowed, Cobblestones, Formations, Frequency, Geology, Gneiss, Granite, Gravel, Gravelly, Harmonic, Igneous, Limestone, Marble, Melodic, Melodious, Metamorphic, Minerals, Musical, Obsidian, Pebbles, Ping, Pitch, Pumice, Quartzite, Resonance, Resonant, Ring, Sandstone, Schist, Scree, Sedimentary, Shale, Shing, Shingles, Slab, Slate, Slates, Sonorous, Sound, Specimens, Stones, Timbre, Tonal, Tone"
|
||||||
|
ROPE,CREAK,ROPECreak,ROPE,"Rope stretches and creaks, people hanging from nooses.. Ship rope creaks. Tension of rope stretches.","Cable, Cord, Creak, Extend, Lengthen, Line, Paracord, Pull, Straighten, Strain, Stretch, Taut, Tension, Tight, Tug, Twine, Yank"
|
||||||
|
ROPE,HANDLE,ROPEHndl,ROPE,"Rope handling, tying knots, rigging up a ship sail. Possible overlap with ROPE-MOVEMENT.","Anchor, Belay, Bend, Bowline, Braid, Cable, Carry, Catch, Clasp, Clench, Clutch, Coil, Cord, Cords, Down, Garrote, Gather, Grab, Grasp, Grip, Gripping, Handle, Hitching, Hold, Knot, Knotting, Lift, Line, Manipulate, Operate, Paracord, Pickup, Pull, Pulling, Rappel, Secure, Seize, Set, Take, Tether, Tethered, Tethering, Throw, Tie, Tied, Toss, Twine, Tying, Untie, Use"
|
||||||
|
ROPE,IMPACT,ROPEImpt,ROPE,"Rope impacts, tossing ropes to ground, ropes banging violently in the wind. Possible overlap with ROPE-MOVEMENT.","Bang, Banging, Bump, Cable, Colliding, Cord, Crashing, Drop, Dropped, Flog, Flogging, Hit, Hitting, Impact, Impacting, Line, Noose, Paracord, Slam, Slamming, Slap, Smack, Smacking, Strike, Striking, Thump, Twine"
|
||||||
|
ROPE,MISC,ROPEMisc,ROPE,Rope sounds not fitting another category in this list.,"Braids, Cable, Cables, Cord, Cordage, Cords, Halyard, Haul, Hawser, Knots, Lariat, Lash, Lashing, Line, Miscellaneous, Paracord, Rigging, Strand, String, Tether, Thread, Twine, Wire, Yarn"
|
||||||
|
ROPE,MOVEMENT,ROPEMvmt,ROPE,"Ropes zuzzing, moving across objects, zipline. See also ROPE-HANDLE.","Cable, Coil, Coiling, Cord, Dangle, Flail, Hang, Hanging, Line, Paracord, Slack, Slacking, Sway, Swing, Toss, Twine, Uncoil, Unfurl, Unravel, Unroll, Untangle, Untwist, Unwind, Zip, Zipline, Zuzz"
|
||||||
|
RUBBER,CRASH & DEBRIS,RUBRCrsh,RUBR,"Rubber items crashing, or rubber debris landing.","Collision, Debris, Elastic, Fragments, Latex, Neoprene, Pieces, Plop, Remains, Rubble, Ruins, Scatter, Shards, Silicone, Smash, Wreckage"
|
||||||
|
RUBBER,FRICTION,RUBRFric,RUBR,"Rubber squeaks, balloon squeaks, a rubber shoe squeak.","Abrasion, Creak, Drag, Elastic, Grinding, Groan, Latex, Neoprene, Rasping, Rub, Rubbing, Scrape, Scraping, Scratching, Screech, Scuffing, Silicone, Sliding, Squeak, Squeaking, Stretch, Stretching, Wear"
|
||||||
|
RUBBER,HANDLE,RUBRHndl,RUBR,"Rubber handling, rubber objects being manipulated or handled. Some overlap with RUBBER-MOVEMENT","Catch, Clasp, Clench, Clutch, Condom, Down, Elastic, Embrace, Galoshes, Grab, Grasp, Grip, Handle, Hold, Innertube, Latex, Neoprene, Operate, Pickup, Seize, Set, Silicone, Squeeze, Take, Throw, Toss, Use"
|
||||||
|
RUBBER,IMPACT,RUBRImpt,RUBR,"Rubber impacts, drops, bounces. Hitting a balloon or rubber ball.","Bang, Banging, Bash, Bounce, Bump, Clap, Collide, Colliding, Crash, Crashing, Drop, Dropped, Elastic, Hit, Hitting, Impact, Impacting, Jolt, Knock, Latex, Neoprene, Pound, Punch, Ram, Silicone, Slam, Slamming, Smack, Smacking, Strike, Striking, Thrust, Thump"
|
||||||
|
RUBBER,MISC,RUBRMisc,RUBR,Rubber sounds not fitting another category in this list.,"Elastic, Latex, Miscellaneous, Neoprene, Polymer, Silicone, Synthetic"
|
||||||
|
RUBBER,MOVEMENT,RUBRMvmt,RUBR,"Rubber movement, dragging rubber or drag on rubber. Rubber flapping. See also RUBBER-HANDLE.","Bounce, Drag, Elastic, Elasticity, Flap, Flex, Flop, Latex, Move, Movement, Neoprene, Pull, Silicone, Slide, Slip, Strain, Stretch, Tension, Twist, Yank"
|
||||||
|
RUBBER,TONAL,RUBRTonl,RUBR,"Rubber movement with a tonal quality, a musical quality.","Bowed, Elastic, Frequency, Harmonic, Latex, Melodic, Melodious, Musical, Neoprene, Ping, Pitch, Resonance, Resonant, Ring, Shing, Silicone, Sonorous, Sound, Timbre, Tonal, Tone"
|
||||||
|
SCIFI,ALARM,SCIAlrm,SCI,"Overly futuristic alarms, the kind you're hear on a spaceship","Alarm, Alert, Buzzer, Fiction, Indicator, Notification, Sci-Fi, Science, Signal, Warning"
|
||||||
|
SCIFI,COMPUTER,SCICmpt,SCI,"Use Sci-Fi for overtly 'science fiction' type computer sounds, a very futuristic computer. Some overlap with USER INTERFACE.","9000, Accumulator, AI, Binary, Borg, Center, Command, Computer, Console, Fiction, HAL, Holoscreen, Interface, Matrix, MCU, Quantum, Sci-Fi, Science, Skynet, Touchscreen, WOPR"
|
||||||
|
SCIFI,DOOR,SCIDoor,SCI,That Star-Trek type door. Spaceship doors.,"Access, Airlock, Bay, Blast, Doorway, Entrance, Exit, Fiction, Gateway, Hatch, Hatchbay, Opening, Passage, Pod, Portal, Sci-Fi, Science, Teleporter"
|
||||||
|
SCIFI,ENERGY,SCIEnrg,SCI,"Energy particles, vortexes, transporters, holograms, force fields.","Atomic, Fiction, Field, Flux, Force, Forcefield, Hologram, Impulse, Particle, Power, Propulsion, Sci-Fi, Science, Shield, Solar, Transporter, Vortex"
|
||||||
|
SCIFI,IMPACT,SCIImpt,SCI,"A bolt of SciFi energy, a hi-tech energy impact, more futuristic than just an explosion. Overlap with DESIGNED-IMPACT.","Blast, Collision, Energy, Fiction, Field, Force, Hit, Jolt, Photon, Sci-Fi, Science, Shield, Strike, Wave"
|
||||||
|
SCIFI,MACHINE,SCIMach,SCI,"Future technology machines, transporters, food replicators.","Beam, Drive, Exosuit, Fiction, Food, Holodeck, Hologram, Machine, Replicator, Replicators, Sci-Fi, Science, Screwdriver, Sonic, Teleporter, Time, Tractor, Warp"
|
||||||
|
SCIFI,MECHANISM,SCIMech,SCI,An overly sci-fi sounding gizmo or mechanism. Little unfolding robot or something like that. Similar to MACHINE-MECHANISM but more SciFi.,"Contraption, Device, Fiction, Gadget, Gizmo, Implement, Sci-Fi, Science, Scifi, Tricorder"
|
||||||
|
SCIFI,MISC,SCIMisc,SCI,SCI-FI sounds not fitting another category in this list.,"Fiction, Miscellaneous, Sci-Fi, Science"
|
||||||
|
SCIFI,RETRO,SCIRetro,SCI,"Cheesy sci-fi sounds, like from The Jetsons, The Day the Earth Stood Still. Old TV or film sci-fi sounds.","Cheesy, Classic, Dr, Fiction, Nostalgic, Old, Old-School, Retro, Retrofuturistic, School, Sci-Fi, Science, Theremin, Throwback, Vintage, Who"
|
||||||
|
SCIFI,SPACESHIP,SCIShip,SCI,"Spaceships here, your best Enterprise and Tie Fighter rip-offs! See also SCIFI-VEHICLE for non-spaceships.","Airship, Apollo, Astronaut, Battlecruiser, Cruiser, Destroyer, Enterprise, Extraterrestrial, Falcon, Fiction, Fighter, Galactica, Interstellar, LEM, Millenium, Mothership, Sci-Fi, Science, Shuttle, Space, Spacecraft, Spaceship, Spaceships, Star, Starcruiser, Starship, Tie, Transport, UFO, Vessel, X-Wing, Y-Wing"
|
||||||
|
SCIFI,VEHICLE,SCIVeh,SCI,"Various sci-fi vehicles, like a moon rover, landspeeder, speeder bike, hoverboard, podracer. See also SCIFI-SPACESHIP.","Bike, Buggy, Fiction, Hoverboard, Hovercraft, Landspeeder, Moon, Podracer, Rover, Sci-Fi, Science, Speeder"
|
||||||
|
SCIFI,WEAPON,SCIWeap,SCI,"Lightsabers, exotic sci-fi weapons. Not a 'blaster' which would go in LASERS-GUN.","Advanced, Arm, Armament, Artillery, Blaster, Energy, Exotic, Fiction, Firearm, Futuristic, High-Tech, Intergalactic, Lightsaber, Lightsabers, Munition, Plasma, Sci-Fi, Science, Space-Age"
|
||||||
|
SNOW,CRASH & DEBRIS,SNOWCrsh,SNOW,"Snow big crashes, a big fall of snow, a roof full of snow collapsing. See also NATURAL DISASTER-AVALANCE for the extreme version of this.","Blizzard, Collision, Crash, Debris, Drift, Flurry, Fragments, Powder, Remains, Rubble, Ruins, Shards, Slush, Smash, Snowball, Snowfall, Wreckage"
|
||||||
|
SNOW,FRICTION,SNOWFric,SNOW,"Snow scrapes, squeaks. Skiing on snow, sledding.","Abrasion, Creak, Flurry, Grinding, Powder, Rasping, Rubbing, Scrape, Scraping, Scratching, Screech, Scuffing, Shovel, Sliding, Slush, Squeak, Stress, Wear"
|
||||||
|
SNOW,HANDLE,SNOWHndl,SNOW,"Snow being handled, a snowball being formed, etc. Digging out of show with your hands. Some overlap with SNOW-MOVEMENT.","Catch, Clasp, Clench, Clutch, Down, Embrace, Flurry, Form, Grab, Grasp, Grip, Handle, Hold, Mold, Operate, Pickup, Powder, Seize, Set, Shape, Slush, Take, Throw, Toss, Use"
|
||||||
|
SNOW,IMPACT,SNOWImpt,SNOW,"A big impact of snow, like a roof full of snow falling. Or smaller like a snowball. Jumping into a pile of snow.","Bang, Banging, Bash, Clap, Collapse, Collide, Colliding, Crash, Crashing, Drop, Dropped, Drift, Fall, Flurry, Hit, Hitting, Impacting, Jolt, Knock, Pound, Powder, Ram, Slam, Slamming, Slush, Smack, Smacking, Snow, Snowball, Strike, Striking, Thrust, Topple"
|
||||||
|
SNOW,MISC,SNOWMisc,SNOW,Snow sounds not fitting another category in this list.,"Cover, Flurry, Frost, Miscellaneous, Powder, Sleet, Slush, Snow, Snowdrift, Snowflakes, Snowpack"
|
||||||
|
SNOW,MOVEMENT,SNOWMvmt,SNOW,"Dragging something through snow, rolling in snow. See also SNOW-HANDLE.","Blow, Drag, Drift, Flurry, Fumble, Pile, Plow, Powder, Shovel, Slide, Slush, Snowfall, Wade"
|
||||||
|
SPORTS,COURT,SPRTCourt,SPRT,"Sports played on a court indoors or out, such a basketball, tennis.","Badminton, Basketball, Dodgeball, Dribbling, Fencing, Futsal, Handball, Netball, Paddleball, Pickleball, Racket, Racquetball, Shuffleboard, Squash, Table, Tennis, Volleyball"
|
||||||
|
SPORTS,FIELD,SPRTField,SPRT,Sports played on a field outdoors such as soccer and football.,"American, Archery, Australian, Baseball, Canadian, Cricket, Field, Football, Frisbee, Gaelic, Hockey, Hurling, Infield, Lacrosse, Outfield, Polo, Rugby, Rules, Soccer, Softball, Ultimate"
|
||||||
|
SPORTS,GYM,SPRTGym,SPRT,"Sports played or performed primarily in a gym and/or ring, such as boxing, wrestling, gymnastics. See also SPORTS-INDOOR.","Arts, Bodybuilding, Boxing, Calisthenics, Crossfit, Do, Dojo, Fitness, Fu, Gymnastics, Judo, Karate, Kickboxing, Kung, Kwon, Martial, Pilates, Powerlifting, Strongman, Tae, Training, Weight, Weightlifting, Wrestling, Yoga"
|
||||||
|
SPORTS,INDOOR,SPRTIndor,SPRT,Various indoor sports not able to be categorized elsewhere.,"Billiards, Bowling, Darts, Ping, Pong"
|
||||||
|
SPORTS,MISC,SPRTMisc,SPRT,Sport sounds not fitting another category in this list.,"Athletics, Competition, Exercise, Fitness, Fun, Games, Golf, Health, Play, Recreation, Training, Velodrome, Well-Being"
|
||||||
|
SPORTS,SKATE,SPRTSkate,SPRT,"Wheeled skating sports like skateboarding or roller skating, rollerblading.","Ice, Inline, Push, Roller, Rollerblading, Scooter, Skateboarding, Skatepark, Skater, Skating, X-Games"
|
||||||
|
SPORTS,TRACK & FIELD,SPRTTrck,SPRT,"Track and field sports, as in running, long jump, pole vault. Think common summer Olympic games.","Decathlon, Discus, Hammer, Heptathlon, High, Hurdles, Javelin, Jump, Long, Long-Distance, Marathon, Middle-Distance, Pole, Put, Races, Relay, Running, Runs, Shot, Sprint, Sprinting, Throw, Triple, Vault"
|
||||||
|
SPORTS,WATER,SPRTWatr,SPRT,"Sports played in or on water, swimming, diving. Aquatic sports.","Aquatics, Canoeing, Diving, Jet, Kayaking, Kiteboarding, Kneeboarding, Paddleboarding, Polo, Rafting, Rowing, Sailing, Scuba, Skiing, Snorkeling, Surfing, Swimming, Volleyball, Wakeboarding, Water, Windsurfing"
|
||||||
|
SPORTS,WINTER,SPRTWntr,SPRT,"Sports on snow or ice such as hockey, ice skating, snowboard and skiing.","Biathlon, Bobsled, Bobsledding, Climbing, Cross-Country, Curling, Dog, Fishing, Hockey, Ice, Jumping, Luge, Racing, Skating, Ski, Skiing, Skis, Sled, Sledding, Snowboarding, Snowmobiling, Snowshoeing, Speed"
|
||||||
|
SWOOSHES,SWISH,SWSH,SWSH,"A swish is smaller than a Whoosh. Arm Swishes, Sword Swishes. Small objects make Swishes.","By, Dart, Flash, Flutter, Fly, Glide, Kungfu, Pass, Race, Rush, Swing, Swipe, Swish, Swishy, Swoop, Swoosh, Whir, Whirl, Whiz, Whoosh, Whooshy, Zip, Zoom"
|
||||||
|
SWOOSHES,WHOOSH,WHSH,WHSH,"A whoosh is larger than a swish. A large object would make a Whoosh. Tanks flying over your head, or an asteroid by. See also DESIGNED-WHOOSH for very stylized whooshes.","Dart, Flash, Fly, Glide, Pass, Race, Rush, Swipe, Swishy, Swoop, Swoosh, Whir, Whirl, Whiz, Whoosh, Whooshy, Woosh, Zip, Zoom"
|
||||||
|
TOOLS,GARDEN,TOOLGrdn,TOOL,"Hand tools used in a garden, rakes, hoes, etc. Also see MACHINES-GARDEN for powered machines. See also OBJECTS-GARDEN for non-tools.","Broom, Cultivator, Edger, Fork, Garden, Hand, Hedge, Hoe, Hose, Lawn, Loppers, Mower, Pruner, Pruning, Rake, Shear, Shovel, Spade, Spades, Sprayer, Sprinkler, Tiller, Trimmer, Trowel, Weed, Weeder"
|
||||||
|
TOOLS,HAND,TOOLHand,TOOL,"Hand tools, not requiring power. Hammers, wrenches, pliers. See also TOOLS-GARDEN.","Allen, Bar, Chisel, Clamp, Cutter, Drill, File, Files, Grip, Gun, Hacksaw, Hammer, Hand, Knife, Level, Mallet, Measure, Pliers, Pry, Ratchet, Saw, Scissors, Scraper, Screwdriver, Shears, Snips, Socket, Staple, Tape, Toolbox, Utility, Vise, Wrench"
|
||||||
|
TOOLS,MISC,TOOLMisc,TOOL,Tool sounds not fitting another category in this list.,Miscellaneous
|
||||||
|
TOOLS,PNEUMATIC,TOOLPneu,TOOL,"Air tools, such as jackhammers, air ratchets, nail guns. The air-wrench sound in a tire shop.","Air, Chisel, Compressor, Drill, Grinder, Gun, Guns, Hammer, Hose, Impact, Jackhammer, Nail, Nailer, Pneumatic, Ratchet, Rivet, Sander, Spray, Stapler, Tool, Wrench"
|
||||||
|
TOOLS,POWER,TOOLPowr,TOOL,"Tools that require power. A sander, electric drill, dremel tool, circular saw.","Air, Angle, Belt, Circular, Compressor, Drill, Driver, Grinder, Gun, Heat, Impact, Jigsaw, Miter, Nail, Planer, Power, Press, Reciprocating, Router, Sander, Saw, Screwdriver, Staple, Table, Tool"
|
||||||
|
TOYS,ELECTRONIC,TOYElec,TOY,"Remote controlled car, slot cars. Any kids toy that required power. See also AIRCRAFT-REMOTE CONTROLLED.","Car, Controlled, Device, Digital, Entertainment, Furby, Gadget, Game, Gizmo, Interactive, Play, Remote, Screen, Slot, Tech, Toy, Train, Virtual"
|
||||||
|
TOYS,MECHANICAL,TOYMech,TOY,"Wind up robots, things like that. Toys with some mechanism or mechanical component.","Box, Erector, Jack-in-the-Box, Kaleidoscope, Music, Robot, Slinky, Spring-Loaded, Tops, Toy, Wind-Up, Winding, Windup, Yo-Yo"
|
||||||
|
TOYS,MISC,TOYMisc,TOY,Toy sounds not fitting another category in this list.,"Action, Animal, Ball, Blocks, Building, Construction, Doll, Figure, Legos, Model, Plaything, Plushie, Puppet, Puzzle, Set, Stuffed"
|
||||||
|
TRAINS,BRAKE,TRNBrake,TRN,"Screeching metalic train brake wheels, subway brakes. Usually a piercing sound.","Bake, Brake, Brakes, Braking, Breaks, Grind, Halting, Pads, Rail, Railcar, Railway, Rolling, Screech, Shoes, Stock"
|
||||||
|
TRAINS,CLACK,TRNClak,TRN,"Train clacks isolated, constant clacking of trains passing or running.","Clack, Clattering, Clickety-Clack, Railway, Rattling, Rhythmic, Track, Train, Wheel"
|
||||||
|
TRAINS,DOOR,TRNDoor,TRN,"Train doors, sliding doors in passenger or freight trains. Some overlap with general DOORS category and possibly VEHICLES-DOOR.","Access, Boxcar, Cab, Caboose, Car, Coach, Door, Hatch, Rail, Railway, Subway, Train"
|
||||||
|
TRAINS,DIESEL,TRNDiesl,TRN,"Freight trains, large commercial diesel locomotives.","Diesel, Diesel-Electric, Diesel-Powered, Freight, Locomotive, Passenger, Powerhouse, Railcar, Railway, Train"
|
||||||
|
TRAINS,ELECTRIC,TRNElec,TRN,"Commuter trains, elevated trains, light rail, SUBWAYS have their own category.","City, Commuter, Inter-City, Light, Line, Maglev, Monorail, Rail, Railway, Regional, Train"
|
||||||
|
TRAINS,HIGH SPEED,TRNHspd,TRN,"Hi-Tech trains, Maglev, monorails, Hyperloops, bullet trains.","350, AGV, AVE, Brightline, Bullet, CRH, Eurostar, Express, Hi-Tech, High-Speed, Hyperloop, ICE, Maglev, Monorails, Railway, Rapid, Shinkansen, Talgo, TGV, Train, X2000"
|
||||||
|
TRAINS,HORN,TRNHorn,TRN,"Train horns, subway horns. A steam train whistle would likely go into WHISTLES-MECHANICAL.","Air, Commuter, Electric, Freight, High-Speed, Horn, Light, Locomotive, Passenger, Rail, Railway, Regional, Subway, Train, Tram"
|
||||||
|
TRAINS,INTERIOR,TRNInt,TRN,"Interior trains, from Diesel to Commuter, onboard train sounds.","Aboard, Berth, Cabin, Caboose, Car, Carriage, City, Coach, Commuter, Compartment, Dining, Freight, Inter-City, Light, Line, Lounge, Luggage, Monorail, Observation, Onboard, Overnight, Passenger, Racks, Rail, Railway, Regional, Rider, Saloon, Seating, Sleeper, Subway, Train"
|
||||||
|
TRAINS,MECHANISM,TRNMech,TRN,"Coupling, decoupling and other mechanical sounds relating to trains. Levers, coal tender mechanics that are specific to a train.","Axles, Bearings, Brakes, Coupler, Couplers, Decouple, Railway, Shunt, Shunter, Suspension, Switcher, Wheels"
|
||||||
|
TRAINS,MISC,TRNMisc,TRN,Train sounds not fitting another category in this list.,"Miscellaneous, Railway"
|
||||||
|
TRAINS,STEAM,TRNSteam,TRN,"Old steam powered trains, big smoke stack chugs.","Boiler, Chuffing, Chugging, Coal, Firebox, Heritage, Hissing, Locomotive, Narrow-Gauge, Piston, Railway, Smokestack, Steam, Steam-Powered, Steaming, Tender"
|
||||||
|
TRAINS,SUBWAY,TRNSbwy,TRN,"Subways, overtly 'underground' sounding trains, above group city trains probably best in TRAINS-ELECTRIC","City, Commuter, Electric, Mass, Metro, MRT, Railcar, Railway, Rapid, Subway, System, Train, Transit, Tube, Tubes, Underground, Urban"
|
||||||
|
TRAINS,TRAM,TRNTram,TRN,"Trolleys, cable cars trams and streetcars. A Tram is 'pulled' along a cable, so not self-powered. An electric tram would go to TRAINS-ELECTRIC.","Cable, Car, Funicular, Grip, Light, Mover, People, Rail, Railway, Streetcar, Tram, Tramcar, Tramway, Trolley, Trolleybus"
|
||||||
|
USER INTERFACE,ALERT,UIAlert,UI,"An alert tone or chime or notification. Something that gets Attention of User, startup chimes.","Advisory, Alarm, Alert, Alertness, Attention, Blip, Caution, GUI, Message, Notice, Notification, Pop-Up, Prompt, Signal, Sound, Startup, Text, Tone, UI, UX, Warning"
|
||||||
|
USER INTERFACE,BEEP,UIBeep,UI,"Interface beeps, beeps while interacting with various technology, phones, computers. Also see BEEPS category.","Alert, Audible, Audio, Beep, Bleep, Boop, Chime, Computer, GUI, Notification, Phone, Prompt, Signal, Sound, Tone, UI, UX, Warning"
|
||||||
|
USER INTERFACE,CLICK,UIClick,UI,"Typing clicks, button press clicks, response to input.","Button, Choose, Click, GUI, Interaction, Interface, Menu, Navigation, Pick, Press, Select, Tap, Type, UI, UX"
|
||||||
|
USER INTERFACE,DATA,UIData,UI,"User interface calculations, scanning, thinking, text displayed on screen. Screen gak or garble.","Buffer, Bytes, Content, Data, Document, Download, File, GUI, Information, Input, Network, Processing, Record, Resource, Thinking, Transfer, UI, Upload, UX"
|
||||||
|
USER INTERFACE,GLITCH,UIGlitch,UI,On Screen Glitches or Static. Malfunctioning computer or OS.,"Abnormality, Anomaly, Bug, Corrupt, Corruption, Crash, Defect, Distortion, Error, Failure, Fault, Flaw, Glitch, Glitchiness, GUI, Interference, Issue, Kernel, Malfunction, Message, Noise, Panic, Problem, Report, Static, UI, UX"
|
||||||
|
USER INTERFACE,MISC,UIMisc,UI,User Interface sounds not fitting another category in this list.,"GUI, Miscellaneous, UI, UX"
|
||||||
|
USER INTERFACE,MOTION,UIMvmt,UI,"User interface slides, swipes, scrolls. Open, Close, Appear, DIsappear.","Action, Activity, Animation, Choose, Confirm, Drag, Dynamics, Flick, GUI, Motion, Movement, Navigation, Page, Scroll, Swipe, Transition, UI, UX, Zoom"
|
||||||
|
VEGETATION,GRASS,VEGEGras,VEGE,Individual movement of grass elements in a studio or exterior environment.,"Flora, Foliage, Grass, Grassland, Green, Greenery, Hay, Lawn, Meadow, Moss, Mow, Mown, Pasture, Rustle, Sod, Through, Turf, Walk, Weeds, Wheat"
|
||||||
|
VEGETATION,LEAVES,VEGELeaf,VEGE,Individual movement of leaf elements in a studio or exterior environment. Jumping into a pile of leaves.,"Alder, Apple, Ash, Aspen, Beech, Birch, Boxwood, Buckeye, Bushes, Cedar, Cherry, Chestnut, Cypress, Dogwood, Elm, Fall, Falling, Fern, Fir, Flora, Foliage, Fronds, Greenery, Hemlock, Hickory, Larch, Leaf, Leafage, Leaves, Magnolia, Mahogany, Maple, Oak, Petals, Pile, Pine, Poplar, Redwood, Scrub, Sequoia, Shoots, Shrub, Shrubbery, Spruce, Stems, Swirl, Sycamore, Thicket, Underbrush, Undergrowth, Willow, Yew"
|
||||||
|
VEGETATION,MISC,VEGEMisc,VEGE,Vegetation and Foliage sounds not fitting another category in this list.,"Botanical, Flora, Foliage, Greenery, Plant, Shrub, Shrubs"
|
||||||
|
VEGETATION,TREE,VEGETree,VEGE,Individual movement of tree and branch elements in a studio or exterior environment.,"Alder, Apple, Ash, Aspen, Bark, Beech, Birch, Boughs, Boxwood, Branch, Branches, Break, Buckeye, Canopy, Cedar, Cherry, Chestnut, Conifer, Crown, Cypress, Dogwood, Elm, Eucalyptus, Fall, Fir, Foliage, Forest, Greenery, Hemlock, Hickory, Larch, Magnolia, Mahogany, Maple, Oak, Palm, Pine, Poplar, Redwood, Sequoia, Snap, Spruce, Sycamore, Timber, Tree, Trunk, Twig, Willow, Wood, Yew"
|
||||||
|
VEHICLES,ALARM,VEHAlrm,VEH,"Not sirens, but just Car alarms, car alarm arm and disarm chirps.","Alarm, Anti-Theft, Arm, Auto, Automobile, Car, Chirp, Disarm, Fob, Security, System"
|
||||||
|
VEHICLES,ANTIQUE,VEHAntq,VEH,"Very old vehicles, such a Model A, Model T. Mainly cars, but possible other very old vehicles, trucks, tractors.","A, Antique, Austin, Auto, Automobile, Car, Classic, Collectible, Duesenberg, Historic, Hudson, Model, Nash, Old-Fashioned, Packard, Pierce-Arrow, Rare, Roadster, Studebaker, T, Used, Vintage"
|
||||||
|
VEHICLES,ATV,VEHAtv,VEH,"All-Terrain Vehicle, three and four-wheelers, dune buggies, side by side.","All-Terrain, ATV, Buggy, Dune, Four-Wheeler, Off-Road, Quad, Quadricycle, Side-By-Side, Three-Wheeler, Utility, UTV"
|
||||||
|
VEHICLES,BICYCLE,VEHBike,VEH,"Bicycles, recumbent bikes, unicycles too. Any manner of pedal powered bicycle.","10-Speed, Backpedal, Bicycle, Bike, BMX, Cycle, Derailleur, Downshift, Freewheel, Kickstand, Mountain, Pedal, Pedaler, Racer, Recumbent, Rider, Road, Spoke, Tandem, Ten-Speed, Training, Tricycle, Unicycle, Velocipede, Wheelie, Wheels"
|
||||||
|
VEHICLES,BRAKE,VEHBrake,VEH,Brake squeals and screeches of all kinds. TRAINS-BRAKE has its own category.,"ABS, Anti-Lock, Auto, Automobile, Brake, Brakes, Car, Decelerate, Disc, Drum, Grind, Halt, Hydraulic, Rotor, Screech, Squeal, Stop"
|
||||||
|
VEHICLES,BUS,VEHBus,VEH,"City buses, school buses, Greyhound buses.","Bus, Buses, City, Coach, Coaches, Double-Decker, Fleet, Greyhound, School, Shuttle, Sightseeing, Tour, Transit"
|
||||||
|
VEHICLES,CAR,VEHCar,VEH,"All cars except overtly antique... add ERA to metadata to make searches easier. 1950s, 1960s...","Auto, Automobile, Automobiles, Autos, Car, Compact, Convertible, Coupe, Crossover, Hatchback, Luxury, Rental, Sedan, Sports, Station, Subcompact, Taxi, Wagon"
|
||||||
|
VEHICLES,CONSTRUCTION,VEHCnst,VEH,"Cranes, bulldozers, paving vehicles, any construction related vehicle goes here.","Backhoe, Bulldozer, Concrete, Constructor, Crane, Digger, Dump, Excavator, Forklift, Grader, Loader, Mixer, Paver, Road, Roller, Skid, Truck, Wrecker"
|
||||||
|
VEHICLES,DOOR,VEHDoor,VEH,"Car, van, truck. Their doors all go here.","Auto, Automobile, Car, Close, Compact, Convertible, Coupe, Crossover, Door, Drivers, Hatchback,Hood, Liftgate, Luxury, Open, Passenger, Rental, Sedan, Sports, Station, Subcompact, Taxi, Tailgate, Slam, SUV, Truck, Trunk, Van"
|
||||||
|
VEHICLES,ELECTRIC,VEHElec,VEH,"Electric golf carts, electric scooters, electric cars. A Tesla. Any variety of electric vehicle, even electric motorcycles.","Auto, Automobile, Car, Cart, E-Bike, Electric, Electrified, EV, Go-Kart, Gold, Golf, Hybrid, Motorcycle, Plug-In, Rivian, Scooter, Segway, Tesla"
|
||||||
|
VEHICLES,EMERGENCY,VEHEmrg,VEH,"Specifically emegency vehicles like Firetrucks, Ambulances, Police cars. Isolated sirens would go still to VEHICLES-SIREN.","Ambulance, Auto, Automobile, Car, Engine, Fire, Firetruck, First, Hazmat, K9, Paramedic, Patrol, Police, Rescue, Responder, Search, SWAT, Truck, Unit, Van, Vehicle"
|
||||||
|
VEHICLES,FARM,VEHFarm,VEH,"I'd put farm tractors here, Some overlap possibly with CONSTRUCTION vehicles. A riding lawn mower would best to MACHINES-GARDEN maybe.","Agricultural, Bale, Carrier, Cart, Combine, Crop, Cultivator, Drill, Duster, Equipment, Farm, Fertilizer, Forage, Grain, Harvester, Hay, Husker, Irrigation, Irrigator, Livestock, Manure, Plow, Processor, Rake, Seed, Seeder, Sprayer, Spreader, Sprinkler, Tiller, Tractor, Tractors, Truck, Wagon"
|
||||||
|
VEHICLES,FREIGHT,VEHFrght,VEH,"Big Rigs, Lorry, Semi-truck, U-Haul trucks, not for TRAINS, which have their own category.","Big, Box, Cargo, Delivery, Flatbed, Freight, Hauler, Hauling, Livestock, Logging, Lorry, Moving, Penske, Rigs, Semi, Semi-Truck, Shipping, Tanker, Transport, Truck, Trucked, Trucking, Trucks, U-Haul"
|
||||||
|
VEHICLES,GENERIC BY,VEHBy,VEH,"Generic or general vehicle pass bys, not 'traffic'. Bys for a specific car go with that car. For group passbys use AMBIENCE-TRAFFIC.","Auto, Automobile, By, Car, Pass, Passing"
|
||||||
|
VEHICLES,HORN,VEHHorn,VEH,Specifically car and vehicles horns. BOATS-HORNS and TRAINS-HORN have their own category. Fog horn would go into HORNS-AIR.,"Auto, Automobile, Beep, Bus, Car, Honk, Honker, Hooter, Horn, Hour, Motorcycle, Rush, Semi, SUV, Toot, Truck"
|
||||||
|
VEHICLES,INTERIOR,VEHInt,VEH,"Interior recordings (ambiences) for car, truck, van interiors, from passenger cars, to back of a moving truck, riding inside a bus.","Auto, Automobile, Bus, Car, Driver, Driving, Inside, Interior, Limousine, Onboard, Passenger, Seat, Semi, Truck, Van, Vehicle"
|
||||||
|
VEHICLES,JALOPY,VEHJalop,VEH,"Backfiring, poorly running vehicle. False starts, sputtering... In foreign language translate to MALFUNCTIONING. See also CARTOON-VEHICLE.","Auto, Automobile, Backfire, Banger, Beater, Bucket, Car, Clunker, FALSE, Heap, Hooptie, Hoopty, Jalopy, Junk, Junker, Lemon, Malfunctioning, Misfire, Misfiring, Old, Ramshackle, Rattletrap, Relic, Rust, Rustbucket, Scrap, Start, Wreck"
|
||||||
|
VEHICLES,MECHANISM,VEHMech,VEH,"Various mechanisms on a vehicle, like seatbelts, window cranks, door locks.","Adjust, Auto, Automobile, Belt, Box, Brake, Car, Choke, Climate, Clutch, Compartment, Control, Crank, Gas Cap, Gear, Gearshift, Glove, Glovebox, Handbrake, Headlights, Ignition, Key, Lever, Mirror, Oil Cap, Parking, Rearview, Seat, Seatbelt, Shift, Side, Signal, Steering, Throttle, Trunk, Turn, Vent, Wheel, Window, Windshield, Wiper"
|
||||||
|
VEHICLES,MILITARY,VEHMil,VEH,"Overtly military ground vehicles. Tanks and military trucks for example. Armored Personnel Carriers, military Jeeps. See also AIRCRAFT-MILITARY and BOATS-MILITARY.","Amphibious, APC, Armored, Army, Carrier, Convoy, Hummer, Humvee, Jeep, Jeeps, Military, Panzer, Personnel, Tactical, Tank, Tanks, Transport, Troops"
|
||||||
|
VEHICLES,MISC,VEHMisc,VEH,Vehicles sounds not fitting another category in this list.,"Auto, Automobile, Car, Limousine, Snowmobile"
|
||||||
|
VEHICLES,MOTORCYCLE,VEHMoto,VEH,"Scooters, Motorcycles, Vespas, Motocross… All things 2 wheeled and powered. ATV goes to VEHICLES-ATV.","Bike, Bikers, BMW, Chopper, Cruiser, Dirt, Dirtbike, Ducati, Handlebar, Harley, Hog, Honda, Indian, Kawasaki, KTM, Minibike, Moped, Mopeds, Moto, Motocross, Motorbike, Motorbikes, Motorcycle, Motorcyclists, Scooter, Scooters, Scrambler, Sidecar, Sport, Superbike, Supermotard, Suzuki, Touring, Triumph, Two-Wheeler, Vespa, Yamaha"
|
||||||
|
VEHICLES,RACING,VEHRace,VEH,"Racing cars or drag racing cars. Indy and Formula 1 cars, monster trucks, BOATS-RACING has its own category.","1, Auto, Automobile, Car, Drag, Dragster, F1, Formula, Grand, Indy, Monster, NASCAR, Prix, Race, Racecar, Rally, Sports, Stock, Supercar, Track, Truck"
|
||||||
|
VEHICLES,SIREN,VEHSirn,VEH,"Police, Firetruck, Ambulance sirens. Any vehicle siren. See also VEHICLES-EMERGENCY if engine sounds are present also.","Air, Alarm, Ambulance, Auto, Automobile, Blare, Brazen, Call, Car, Cops, Emergency, Fighter, Fire, Firetruck, Hi-Lo, Hooter, Horn, Howler, Piercer, Police, Power, Siren, Truck, Wail, Warning, Whoop, Yelp"
|
||||||
|
VEHICLES,SKID,VEHSkid,VEH,"Tire skids, peel-outs. Other tire sounds go in VEHICLES-TIRES.","ABS, Auto, Automobile, Burnout, Car, Careening, Chuff, Drift, Fishtail, Marks, Out, Peel, Screech, Skid, Skidmark, Slide, Spin, Squeal, Swerve, Tire"
|
||||||
|
VEHICLES,SUSPENSION,VEHSusp,VEH,"Suspension squeaks and rattles, for any car, truck, semi. Suspensions springs. A car or truck on a very rough road.","Absorber, Absorbers, Air, Anti-Roll, Arm, Arms, Auto, Automobile, Bar, Bump, Car, Chassis, Coilovers, Control, Dampers, Independent, Leaf, Macpherson, Multi-Link, Pothole, Rattle, Shock, Speed, Springs, Squeak, Stabilizer, Strut, Struts, Suspension, Sway, Torsion, Wishbone"
|
||||||
|
VEHICLES,TIRE,VEHTire,VEH,"Tire rolls on various surfaces, tire whines, no engine sounds, isolated tire sounds. A flat tire flop.","All-Season, Bicycle, Blackwall, Bridgestone, Car, Dunlops, Firestone, Firestones, Flat, Goodyears, Hubcap, Innertube, Michelin, Motorcycle, Noise, Off-Road, Performance, Pirelli, Puncture, Racing, Radial, Rim, Rims, Road, Roll, Rubber, Run-Flat, Sidewall, Snow, Spare, Summer, Tire, Tires, Tread, Truck, Tubeless, Tyres, Wheel, Wheels, Whitewall, Winter"
|
||||||
|
VEHICLES,TRUCK VAN & SUV,VEHTruck,VEH,"Passenger Trucks, pickup trucks, Vans and SUVs. Commercial trucks go in VEHICLES-FREIGHT.","Box, Camper, Cargo, Conversion, Cream, Delivery, Flatbed, Food, Ice, Microbus, Mini, Minibus, Minivan, Paddy, Panel, Passenger, Pickup, RV, Step, SUV, Tow, Truck, Van, Vans, Work"
|
||||||
|
VEHICLES,UTILITY,VEHUtil,VEH,"Golf carts, cushmans, traffic, gator, small motorized utility vehicles.","By, Cushman, Gator, Ranger, RZR, Side, SXS, Ute, UTV"
|
||||||
|
VEHICLES,WAGON,VEHWagn,VEH,"Old wood wagon, horse drawn carts, carriages, buggies. A Stagecoach.","Amish, Buckboard, Buggy, Carriage, Carriages, Cart, Chariot, Drawbar, Hayride, Ox, Stagecoach, Waggon, Wagon, Wagons, Wain, Wood"
|
||||||
|
VEHICLES,WINDOW,VEHWndw,VEH,"Powered windows for any vehicles, crank windows should go in VEHICLES-MECHANISM.","Auto, Automatic, Automobile, Car, Drivers, Down, Passenger, Power, Roll, Up, Window"
|
||||||
|
VOICES,ALIEN,VOXAlien,VOX,Weird alien vocals here.,"Alien, Chewbacca, Cosmic, ET, Extraterrestrial, Language, Vocal, Vocalization"
|
||||||
|
VOICES,BABY,VOXBaby,VOX,"Very young babies through toddler, non-word vocals, cut vocals.","Baba, Babble, Bambino, Coo, Cooing, Dada, Fuss, Gaga, Infant, Infantile, Little, Mama, Newborn, Nursing, Nursling, One, Snuffle, Toddler, Tot, Tyke, Vocal, Vocalization, Wean"
|
||||||
|
VOICES,CHEER,VOXCheer,VOX,A single person cheering. For a group use CROWDS-CHEERING.,"Acclaim, Acclamation, Bravo, Celebrating, Cheer, Cheering, Commendation, Encouraging, Holler, Hollering, Horray, Hurrah, Huzzah, Kudos, Ovation, Rooting, Shout, Supporting, Vocal, Vocalization, Woohoo, Yay, Yelling"
|
||||||
|
VOICES,CHILD,VOXChld,VOX,Various vocals for a child or a teenager. For non-discernible vocals for a very young child use VOICES-BABY.,"Adolescent, Child, Children, Juvenile, Kid, Minor, One, Preteen, Pubescent, Teen, Toddler, Tween, Vocal, Vocalization, Young, Youngling, Youngster, Youth"
|
||||||
|
VOICES,CRYING,VOXCry,VOX,A single person crying.,"Bawl, Bawling, Bemoan, Blubber, Blubbering, Cry, Fuss, Howling, Lamenting, Pout, Sniffling, Sniveling, Sob, Sobbing, Vocal, Vocalization, Wail, Wailing, Weep, Weeping, Whimper, Whimpering, Whining, Wounded"
|
||||||
|
VOICES,EFFORTS,VOXEfrt,VOX,"A single fight and vocal efforts, single grunts, and things like that. Boxing match vocals, tennis grunts.","Effort, Exert, Exertion, Exhale, Gasping, Grunt, Grunting, Heaving, Inhale, Panting, Pushing, Strain, Struggle, Struggling, Vocal, Vocalization, Wheezing"
|
||||||
|
VOICES,FEMALE,VOXFem,VOX,Any generic female vocal or dialog or misc vocal that doesn't fit somewhere else.,"Chick, Dame, Female, Feminine, Gal, Girl, Lady, Lass, Madam, Miss, Person, Vocal, Vocalization, Woman"
|
||||||
|
VOICES,FUTZED,VOXFutz,VOX,"P.A. Speakers, Airport announcements, Hospital announcements.","Address, Altered, Announcement, Distorted, Manipulated, Megaphone, Modified, PA, Phone, Processed, Public, Radio, Speaker, Telephone, TV, Vocal, Vocalization, Walkie-Talkie"
|
||||||
|
VOICES,HISTORICAL,VOXHist,VOX,"For storing famous speeches, famous news broadcasts, presidential speeches.","Annoucement, Broadcast, Decree, Edict, Historical, Lecture, Manifesto, News, Period, Proclamation, Pronouncement, Speech, Statement, Vintage, Vocal, Vocalization"
|
||||||
|
VOICES,LAUGH,VOXLaff,VOX,A single person laughing.,"Belly, Cackle, Cackling, Chortle, Chortling, Chuckle, Chuckling, Funny, Giggle, Giggling, Guffaw, Guffawing, Haha, Holler, Hoot, Humor, Humour, Hysterical, Joke, Laugh, Laughing, Maniacal, Snicker, Snickering, Snigger, Sniggering, Titter, Tittering, Twitter, Vocal, Vocalization"
|
||||||
|
VOICES,MALE,VOXMale,VOX,Any generic male vocal or dialog or misc vocal that doesn't fit somewhere else.,"Bloke, Boy, Brother, Chap, Dude, Fellow, Gentleman, Guy, Male, Man, Masculine, Person, Vocal, Vocalization"
|
||||||
|
VOICES,MISC,VOXMisc,VOX,The Voice category is for single voices. Groups of people go into the CROWDS categories.,"Miscellaneous, Vocal, Vocalization"
|
||||||
|
VOICES,REACTION,VOXReac,VOX,A single Ooo or Aah and things of that nature.,"Aahs, Acknowledgement, Ah, Ahh, Ahhh, Answer, Applause, Booing, Boos, Chanting, Cheers, Comeback, Counter, Em, Er, Excited, Feedback, Gasps, Hm, Hmm, Hollers, Hoots, Laughter, Murmurs, Oh, Ooh, Oohs, Ooo, Reaction, Rejoinder, Reply, Response, Retort, Sighs, Vocal, Vocalization, Whistling"
|
||||||
|
VOICES,SCREAM,VOXScrm,VOX,"A single scream or yell, battle cry, agonizing death.","Bellow, Clamour, Death, Fall, Hollar, Holler, Howl, Outcry, Scream, Screech, Shout, Shriek, Squeal, Vocal, Vocalization, Wail, Wilhelm, Yell, Yelp, Yowl"
|
||||||
|
VOICES,SINGING,VOXSing,VOX,"A single voice singing, a child singing. For a crowd, use CROWDS-SINGING","Acappella, Caroling, Chanting, Chorusing, Crooning, Ditty, Hum, Hymning, Melodizing, Performing, Serenading, Sing, Vocal, Vocalization, Vocalizing"
|
||||||
|
VOICES,WHISPER,VOXWhsp,VOX,A single whispering voice. The crowd equivalent would be CROWDS-QUIET.,"Faint, Gossip, Grumble, Grumbling, Hush, Hushed, Mumble, Murmur, Mutter, Muttering, Quietly, Secret, Softly, Speak, Subdued, Susurration, Undertones, Vocal, Vocalization, Whisper"
|
||||||
|
WATER,BUBBLES,WATRBubl,WATR,"Single or continuous bubbles. Water boiling on a stove, fish tank bubbles.","Aerate, Aerated, Aeration, Aqua, Blub, Boil, Boiling, Boils, Bubble, Bubbler, Bubbling, Bubbly, Carbonation, Cauldron, Cavitation, Effervesce, Effervescence, Effervescent, Foam, Froth, Frothy, Glub, Gurgling, H20, Hissing, Potion, Scuba, Simmer"
|
||||||
|
WATER,DRAIN,WATRDran,WATR,"Draining gurgling water, pulling in air with it as it drains, think a kitchen sink drain, or a storm drain, shower drain.","Aqua, Burble, Culvert, Discharge, Diverter, Downspout, Downspouts, Drain, Drainage, Drainages, Draining, Drainpipes, Drains, Flow, Gurgle, Gutter, H20, Leaking, Ooze, Outflow, Outlet, Pipe, Runoff, Seepage, Sewer, Shower, Sink, Storm, Stream, Trickle"
|
||||||
|
WATER,DRIP,WATRDrip,WATR,"Water dripping, slow to fast. A cave drip, a leaking pipe dripping or a kitchen faucet dripping.","Aqua, Cave, Dribble, Drip, Dripping, Drizzle, Drop, H20, Leak, Leaking, Plip, Plop, Seep, Splash, Sprinkle, Trickle"
|
||||||
|
WATER,FIZZ,WATRFizz,WATR,"That sound of Alka-Seltzer in a cup, air fizzing in water. Carbonated beverages.","Aqua, Bubble, Bubbling, Carbonation, Coke, Cola, Crackling, Effervescence, Fizz, Fizzle, Fizzling, Fizzy, Foam, Foaming, Froth, H20, Hiss, Hissing, Perrier, Popping, Seltzer, Sizzle, Sizzling, Snapping, Soda, Sparkle, Sparkling"
|
||||||
|
WATER,FLOW,WATRFlow,WATR,"General water flowing, in our case as in a river, creek or stream. Steady water. Rapids would fall under WATER-TURBULENCE, and WATERFALL has its own subcategory","Aqua, Brook, Creek, Current, Gully, H20, Rill, River, Rivulet, Runnel, Running, Spring, Stream, Tributary, Water, Watercourse"
|
||||||
|
WATER,FOUNTAIN,WATRFoun,WATR,"Water features and fountains, trickles or flows for a fountain, splashy fountains. From tiny to large city fountains.","Aqua, Birdbath, Decorative, Feature, Flow, Fountain, H20, Hiss, Ornamental, Spout, Spray, Trickle, Water, Well, Wishing"
|
||||||
|
WATER,IMPACT,WATRImpt,WATR,"Percussive water impact, large crash or slam of water, as in huge wave hitting an object. Bigger and sharper than a SPLASH","Aqua, Belly, Bellyflop, Cannonball, Charge, Collide, Crash, Depth, Flop, H20, Hit, Slam, Splash, Sploosh"
|
||||||
|
WATER,LAP,WATRLap,WATR,"Water splashing against something like against the side of a boat or canoe, or a dock. Also meant for very small waves lapping on shore, like gentle very small waves at night.","Aqua, H20, Hull, Lap, Lapping, Ripple, Slap, Slosh, Sloshing"
|
||||||
|
WATER,MISC,WATRMisc,WATR,Water sounds not fitting another category in this list.,"Aqua, H20, Miscellaneous"
|
||||||
|
WATER,MOVEMENT,WATRMvmt,WATR,"Movement in water but not underwater (see WATER-UNDERWATER) Drags through water, swimming movements, sweeps of objects through water. Thrashing in water.","Aqua, Bobbing, Churning, Ebbing, Eddying, H20, Rippling, Slosh, Splash, Stirring, Sweep, Swirling, Tread, Wade"
|
||||||
|
WATER,PLUMBING,WATRPlmb,WATR,"Household plumbing, like a sink running, a toilet flush, the shower running. DRAIN has its own subcategory.","Aqua, Aqueduct, Auger, Bathtub, Bidet, Channel, Clog, Commode, Conduit, Crapper, Drain, Drainpipe, Faucet, Fixtures, Flush, Grease, Gutter, H20, P-Trap, Pipage, Pipeline, Pipes, Piping, Plumber, Plumbing, Septic, Sewage, Sewer, Shower, Sink, Spout, Tank, Toilet, Trap, Trough, Valve, Waterworks"
|
||||||
|
WATER,POUR,WATRPour,WATR,"Pouring water, onto ground, into a cup. Overlap with FOOD & DRINK-POUR.","Aqua, Bathe, Discharged, Discharging, Dispense, Douse, Dousing, Dowse, Dowsing, Drench, Drenched, Dump, Empty, Fill, Flow, Gush, H20, Overflow, Pour, Pouring, Sluice, Sluicing, Spill"
|
||||||
|
WATER,SPLASH,WATRSplsh,WATR,"Water splashes of all kind, a person diving into a pool, a whale cresting, throwing a rock into water.","Aqua, Dive, H20, Kerplunk, Plunge, Showering, Slosh, Spattering, Splash, Splattering, Splish, Splosh, Spraying, Submerge, Swim, Wade"
|
||||||
|
WATER,SPRAY,WATRSpray,WATR,"Big sprays of a wave crash, to a hose spray.","Aqua, Gush, H20, Hose, Hosed, Irrigate, Mist, Spray, Sprinkle, Spritz, Squirt"
|
||||||
|
WATER,STEAM,WATRStm,WATR,"Very wet hisses, radiators, saunas. Hot wet steam.","Aqua, Condensation, Evaporation, Fog, H20, Hiss, Mist, Spritz, Sputter, Steam, Superheat, Superheating, Vapor, Vaporization, Wet"
|
||||||
|
WATER,SURF,WATRSurf,WATR,"The general roar of the ocean, not the sport. Constant ocean roaring waves.","Aqua, Beach, Billows, Breakers, Coastline, H20, Rollers, Seashore, Shoreline, Surf, Surge, Swell, Tide, Wash, Waves, Whitecaps"
|
||||||
|
WATER,TURBULENT,WATRTurb,WATR,"Very big splashy water. Whirlpools, big rapids on a river. Dangerous water.","Agitated, Aqua, Choppy, Churning, Current, H20, Maelstrom, Pool, Raging, Rapids, Riptide, Roil, Roiled, Rough, Squally, Stormy, Swells, Swirl, Swirling, Tempestuous, Torrential, Tumultuous, Turbulent, Undertow, Violent, Water, Wave, Whirlpool, White, Whitecaps"
|
||||||
|
WATER,UNDERWATER,WATRUndwtr,WATR,"Specifically underwater sounds, flows, current and movement, Swimming underwater. See also AMBIENCE-UNDERWATER and WATER-MOVEMENT as well.","Aqua, Aquatic, Engulfed, Flooded, Flow, H20, Immersed, Inundated, Plunged, Subaquatic, Subaqueous, Submerged, Submersed, Subsea, Sunken, Undersea, Underwater"
|
||||||
|
WATER,WATERFALL,WATRFall,WATR,Waterfalls from small cascading ones to Niagara falls.,"Aqua, Cascade, Cascading, Cataract, Dam, Falls, H20, Niagara, Plunge, Rapids, Torrent, Victoria, Waterfall"
|
||||||
|
WATER,WAVE,WATRWave,WATR,"For general water waves, distinct waves. General ocean roar to go in WATER-SURF, and tiny waves to WATER-LAPPING. See also NATURAL DISASTERS-TSUNAMI.","Aqua, Billow, Breaker, Breakers, Breakwater, Crest, Current, H20, Ocean, Ripples, Roller, Sea, Seashore, Surf, Surge, Swell, Swells, Tides, Water, Wave, Waves, Whitecap"
|
||||||
|
WEAPONS,ARMOR,WEAPArmr,WEAP,"Knights armor movement, hits. Shields, chainmail armor, leather armor with buckles.","Armor, Armorer, Armory, Armour, Armoured, Armourer, Armoury, Bracers, Brassard, Breastplate, Buckler, Chainmail, Cuirass, Domaru, Gauntlets, Greaves, Haramaki, Hauberk, Helm, Helmet, Mail, Of, Pauldron, Plate, Sabatons, Scale, Shield, Shields, Spaulders, Splint, Suit, Tabard, Vambrace, Visor"
|
||||||
|
WEAPONS,ARROW,WEAPArro,WEAP,"Arrow slides, arrow impacts. An arrow by would likely be a SWOOSHES-SWISH. See also WEAPONS-BOW.","Archer, Arrow, Arrowhead, Bolt, Bowyer, Crossbow, Dart, Fletch, Fletcher, Fletching, Flight, Nock, Projectile, Quarrel, Quiver, Shaft"
|
||||||
|
WEAPONS,AXE,WEAPAxe,WEAP,Specifically a weaponized ax. Possible overlap with WEAPONS-SWORD or even WEAPON-POLEARM.,"Ax, Axe, Blade, Chop, Chopper, Edge, Handle, Hatchet, Head, Throwing, Tomahawk"
|
||||||
|
WEAPONS,BLUNT,WEAPBlnt,WEAP,"Blunt weapons, like maces and flails, but also nightsticks.","Ball, Bat, Baton, Bludgeon, Brass, Chain, Club, Cudgel, Flail, Hammer, Joust, Knuckles, Mace, Morning, Nightstick, Nunchuck, Star, Stave, Tonfa, Truncheon, War, Warhammer"
|
||||||
|
WEAPONS,BOW,WEAPBow,WEAP,"Bows and Crossbows, articulation, twangs, bolts being loaded. Bow releases. See also WEAPON-ARROW.","Archer, Bow, Bowyer, Box, Compound, Crossbow, Fletcher, Hornbow, Longbow, Recurve, Release, Shortbow, String, Traditional"
|
||||||
|
WEAPONS,KNIFE,WEAPKnif,WEAP,"Smaller knives, WEAPONS-SWORDS has it own subcategory here.","Army, Bayonet, Blade, Bowie, Butcher, Butterfly, Chef, Cleaver, Dagger, Dirk, Hunting, Jack, Kitchen, Knife, Machete, Penknife, Pocket, Pocketknife, Scalpel, Shealth, Steak, Stiletto, Survival, Swiss, Switchblade, Throwing, Trench, Utility"
|
||||||
|
WEAPONS,MISC,WEAPMisc,WEAP,Weapon sounds not fitting another category in this list.,"Boomerang, Slingshot"
|
||||||
|
WEAPONS,POLEARM,WEAPPole,WEAP,"A long spear or long poled weapon with sharp components at the end, like a harpoon.","Fauchard, Glaive, Halberd, Harpoon, Javelin, Lance, Pike, Poleaxe, Pollaxe, Spear"
|
||||||
|
WEAPONS,SIEGE,WEAPSiege,WEAP,"A siege weapon is a device designed to break or circumvent fortifications, catapults, trebuchets. Ancient artillery.","Ballista, Battering, Bombard, Catapult, Engine, Fire, Mangonel, Mangonon, Onager, Onagro, Petraria, Petrary, Ram, Scorpion, Siege, Springald, Torsion, Tower, Trebuchet, Warwolf"
|
||||||
|
WEAPONS,SWORD,WEAPSwrd,WEAP,"Swords, clashing, parrying. Sword rings and hits. STABS would go to GORE category. Sword draws, shings.","Blade, Broadsword, Claymore, Cutlass, Dagger, Epee, Falchion, Fencing, Foil, Foils, Katana, Longsword, Machete, Pommel, Rapier, Saber, Sabre, Samurai, Scabbard, Scimitar, Slashing, Sword, Swords, Viking"
|
||||||
|
WEAPONS,WHIP,WEAPWhip,WEAP,Whip cracks. Make Indiana Jones proud.,"Buggy, Bullwhip, Cat'O'Nine, Crop, Flogger, Horse, Lash, Riding, Scourge, Tails, Thong, Whip, Whipcord, Whipcrack"
|
||||||
|
WEATHER,HAIL,HAIL,HAIL,Hail on various surfaces.,"Balls, Frozen, Graupel, Hail, Hailstones, Hailstorm, Ice, Pellets, Rain, Sleet"
|
||||||
|
WEATHER,MISC,WTHR,WTHR,Weather sounds not fitting another category in this list. See also NATURAL DISASTER category.,"Atmosphere, Atmospheric, Climate, Conditions, Elements, Forecast, Meteorology, Miscellaneous"
|
||||||
|
WEATHER,STORM,STORM,STORM,"Composite storms, wind, rain, thunder, where elements aren't isolated. Mixed storm composite recordings. Also see WIND-TURBULENT for wind only elements and NATURAL-DISASTER.","Blizzard, Cyclone, Electrical, Gale, Hailstorm, Hurricane, Ice, Monsoon, Rainstorm, Sandstorm, Sleet, Snowstorm, Squall, Storm, Supercell, Tempest, Thunderhead, Thunderstorm, Tornado, Typhoon, Windstorm"
|
||||||
|
WEATHER,THUNDER,THUN,THUN,"Thunder and lightning, dry, wet, distant rumbles to sharp thunder cracks. Combination RAIN & THUNDER may best go into WEATHER-STORM","Boom, Clap, Crack, Crash, Lightening, Lightning, Roar, Roll, Rumble, Thunder, Thunderbolt, Thunderclap, Thunderstorm"
|
||||||
|
WHISTLES,HUMAN,WHSTHmn,WHST,"Human whistles, wolf-whistles. See also CARTOON-WHISTLES.","Cheering, Human, Signal, Whistle, Wolf"
|
||||||
|
WHISTLES,MECHANICAL,WHSTMech,WHST,"Train whistles, referee whistles, tea pot whistles, etc, any whistle not made by a human.","Bird, Call, Coach, Dog, Lifeguard, Pea, Police, Referee, Shrill, Steam, Teapot, Toot, Train"
|
||||||
|
WHISTLES,MISC,WHSTMisc,WHST,Whistle sounds not fitting another category in this list.,Miscellaneous
|
||||||
|
WIND,DESIGNED,WINDDsgn,WIND,"Designed or manufactured or artificial winds, wind machines, synth winds.","Artificial, Designed, Machine, Simulated, Synthetic, Tonal, Wind"
|
||||||
|
WIND,GENERAL,WIND,WIND,Base and general wind category.,"Air, Atmospheric, Breeze, Current, Draft, Flow, Miscellaneous, Wind, Windy"
|
||||||
|
WIND,GUST,WINDGust,WIND,"For more singular gusts or buffets, accents to cut into other winds. Usually shorter. A constant buffeting wind better off in the GENERAL or TURBULENT category.","Blast, Blow, Blustery, Breeze, Breezy, Buffet, Buffeting, Crosswind, Downwind, Flurry, Gale, Gales, Gust, Gustation, Gusting, Gusts, Gusty, Headwind, Rush, Strong, Waft, Whirlwinds, Wind, Wisps, Zephyr"
|
||||||
|
WIND,INTERIOR,WINDInt,WIND,"Interior wind, through window, or crack in door. Wind through chimney or vent pipe, but see also WIND-TONAL for overtly tonal winds and whistles.","Abandoned, Breeze, Current, Door, Draft, Gust, House, Interior, Moan, Whistle, Window"
|
||||||
|
WIND,TONAL,WINDTonl,WIND,"Wind howls, wind in wires, wind whistles, overtly tonal winds.","Harmonic, Howl, Howling, Moan, Moaning, Roar, Singing, Tonal, Wail, Whistle"
|
||||||
|
WIND,TURBULENT,WINDTurb,WIND,"Extremely violent wind, big buffeting, huge storm winds. Possible overlap with NATURAL DISASTER-TORNADO or WEATHER-STORM.","Buffet, Choppy, Devil, Dust, Gale, Hurricane, Microburst, Slipstream, Squall, Stormy, Strong, Tempest, Tornado, Turbulence, Turbulent, Twister, Typhoon, Unsteady, Violent, Vortex, Whirlwind"
|
||||||
|
WIND,VEGETATION,WINDVege,WIND,"Wind through foliage, vegetation, grass, trees, leaves, plants.","Bending, Blowing, Branches, Foliage, Grass, Leaf, Leaves, Plant, Rustle, Rustling, Soughing, Swaying, Tree, Trees, Vegetation, Whispering, Wind"
|
||||||
|
WINDOWS,COVERING,WNDWCover,WNDW,"Window curtains, blinds, shutters. So a window covering, not the window itself.","Awnings, Blackout, Blind, Blinds, Curtain, Curtains, Drapes, Mini-Blinds, Panes, Shades, Shutter, Shutters, Valances, Veils, Venetian"
|
||||||
|
WINDOWS,HARDWARE,WNDWHdwr,WNDW,"Window latches, cranks, locks. Window hardware.","Catches, Cranks, Fasteners, Handles, Hinges, Hooks, Latch, Latches, Lock, Locks, Panes, Sash, Slides, Window"
|
||||||
|
WINDOWS,KNOCK,WNDWKnck,WNDW,"Knocking on various window surfaces, knocking on GLASS.","Bang, Knock, Knocking, Pane, Panes, Pound, Pounding, Rap, Rapping, Rattle, Tap, Tapping, Thump"
|
||||||
|
WINDOWS,METAL,WNDWMetl,WNDW,"Metal framed windows, open and close.","Frame, Metal, Panes"
|
||||||
|
WINDOWS,MISC,WNDWMisc,WNDW,Window sounds not fitting another category in this list.,"Miscellaneous, Panes"
|
||||||
|
WINDOWS,PLASTIC,WNDWPlas,WNDW,"Plastic framed windows, plexiglass windows. Vinyl framed windows, open and close.","Frame, Panes, Plastic, Vinyl"
|
||||||
|
WINDOWS,WOOD,WNDWWood,WNDW,"Wood framed windows, open and close.","Frame, Panes, Wood"
|
||||||
|
WINGS,BIRD,WINGBird,WING,"Bird wing flaps, hummingbirds, pigeon wings. Group or single.","Avian, Bird, Feather, Feathered, Flap, Flapping, Flight, Flutter, Flying, Pinion, Wing, Winged, Wingspan, Wingspread"
|
||||||
|
WINGS,CREATURE,WINGCrea,WING,"Dragon wings, fairy wings, any manner of creature and monster wings.","Angel, Bird, Creature, Dragon, Fairy, Fantastical, Giant, Griffin, Legendary, Monster, Mythical, Phoenix, Sphinx, Supernatural, Wing"
|
||||||
|
WINGS,INSECT,WINGInsc,WING,"Fly and bee wings, dragonflies, moths, butterflies. Any insect wings. Some overlap and confusion with ANIMALS-INECTS likely.","Bee, Beetle, Bug, Butterfly, Cicada, Damselfly, Dragonfly, Firefly, Flies, Fly, Gnats, Grasshopper, Hornet, Insectoid, Katydid, Ladybug, Locust, Mayfly, Mosquito, Moth, Scarab, Wasp"
|
||||||
|
WINGS,MISC,WINGMisc,WING,Wing sounds not fitting another category in this list.,"Appendage, Feather, Miscellaneous, Pinion, Winglet, Wingtip"
|
||||||
|
WOOD,BREAK,WOODBrk,WOOD,"Wood breaks, shatters, cracks, crunches. Any destruction of wood that breaks the piece.","2x4, Apart, Beam, Board, Break, Breaks, Burst, Chip, Crack, Cracking, Cracks, Crumble, Crunches, Crush, Demolish, Destroy, Disintegrate, Dowel, Fracture, Fractures, Fragment, Hardwood, Joist, Log, Lumber, Plank, Plywood, Rafter, Rips, Shatter, Shattering, Shatters, Smash, Snap, Snapping, Softwood, Splinter, Splintering, Split, Stud, Timber"
|
||||||
|
WOOD,CRASH & DEBRIS,WOODCrsh,WOOD,"Group wood crashes and debris, and pieces falling, as in after an explosion.","2x4, Beam, Board, Boards, Broken, Collision, Crash, Debris, Dowel, Fall, Fell, Fragments, Hardwood, Joist, Log, Lumber, Plank, Planks, Plywood, Rafter, Remains, Rubble, Ruins, Shards, Smash, Softwood, Splintered, Splinters, Stud, Timber, Wreckage"
|
||||||
|
WOOD,FRICTION,WOODFric,WOOD,"Wood stress, especially creaks. But also scrapes, squeaks.","2x4, Abrasion, Beam, Board, Creaking, Creaks, Dowel, Grating, Grinding, Hardwood, Joist, Log, Lumber, Plank, Plywood, Rafter, Rasping, Rubbing, Scrapes, Scraping, Scratching, Screech, Screeching, Scuffing, Sliding, Softwood, Squeaks, Stress, Stud, Timber, Wear"
|
||||||
|
WOOD,HANDLE,WOODHndl,WOOD,"Handling a wooden object, picking up, setting down, etc. Some overlap or confusion with WOOD-MOVEMENT.","2x4, Beam, Board, Catch, Clasp, Clench, Clutch, Dowel, Down, Embrace, Grab, Grasp, Grip, Handle, Hardwood, Hold, Joist, Log, Lumber, Operate, Pickup, Plank, Plywood, Rafter, Seize, Set, Softwood, Stud, Take, Throw, Timber, Toss, Use"
|
||||||
|
WOOD,IMPACT,WOODImpt,WOOD,"Singular wood hits, drops, impacts, board hits. Group wood destruction goes into CRASH. Resonant wood bonks or hits. Hitting your head on a doorframe.","2x4, Bang, Banging, Bash, Beam, Blow, Board, Bonk, Chop, Clang, Clap, Clink, Clunk, Collide, Colliding, Collision, Conk, Crash, Crashing, Dowel, Drop, Dropped, Hardwood, Hit, Hitting, Impact, Impacting, Joist, Jolt, Knock, Log, Lumber, Plank, Plywood, Pound, Rafter, Ram, Slam, Slamming, Smack, Smacking, Softwood, Strike, Striking, Stud, Thrust, Timber"
|
||||||
|
WOOD,MISC,WOODMisc,WOOD,Wood sounds not fitting another category in this list.,"2x4, Beam, Board, Dowel, Hardwood, Joist, Log, Lumber, Miscellaneous, Plank, Plywood, Rafter, Softwood, Stud, Timber, Tree, Wooden"
|
||||||
|
WOOD,MOVEMENT,WOODMvmt,WOOD,"Wood movement including rattles, drag, slides, rolls. Group pieces of wood moving together.","2x4, Beam, Bending, Board, Bowing, Checking, Collapse, Contraction, Cracking, Crowning, Cupping, Deformation, Dowel, Drag, Expansion, Hardwood, Joist, Log, Lumber, Piling, Plank, Plywood, Rafter, Rattle, Rattling, Roll, Shake, Shaking, Shifting, Shrinkage, Softwood, Splitting, Stacking, Stud, Swelling, Timber, Tossing, Twisting, Warping"
|
||||||
|
WOOD,TONAL,WOODTonl,WOOD,"Tonal bowed wood sounds, ringing wood.","2x4, Beam, Board, Bowed, Dowel, Frequency, Hardwood, Harmonic, Joist, Log, Lumber, Melodic, Melodious, Musical, Ping, Pitch, Plank, Plywood, Rafter, Resonance, Resonant, Ring, Shing, Softwood, Sonorous, Sound, Stud, Timber, Timbre, Tonal, Tone"
|
||||||
|
47
package.json
Normal file
47
package.json
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "audio-sort",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Electron audio sorting application with fuzzy search, tagging, and library management for WAV files.",
|
||||||
|
"main": "dist/main/main/index.js",
|
||||||
|
"type": "commonjs",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "concurrently -k \"npm:dev:renderer\" \"npm:dev:main\" \"npm:dev:electron\"",
|
||||||
|
"dev:renderer": "vite --config vite.config.mjs",
|
||||||
|
"dev:main": "tsc -p tsconfig.main.json --watch --preserveWatchOutput",
|
||||||
|
"dev:electron": "cross-env NODE_ENV=development VITE_DEV_SERVER_URL=http://localhost:5173 node ./scripts/start-electron.mjs",
|
||||||
|
"build": "npm run build:renderer && npm run build:main",
|
||||||
|
"build:renderer": "vite build --config vite.config.mjs",
|
||||||
|
"build:main": "tsc -p tsconfig.main.json",
|
||||||
|
"lint": "tsc --noEmit",
|
||||||
|
"start": "cross-env NODE_ENV=production electron ./dist/main/main/index.js",
|
||||||
|
"test": "node --import tsx --test src/test/DatabaseService.test.ts src/test/TagService.test.ts src/test/LibraryService.test.ts src/test/SearchService.test.ts",
|
||||||
|
"test:tag": "node --import tsx --test src/test/TagService.test.ts",
|
||||||
|
"test:library": "node --import tsx --test src/test/LibraryService.test.ts",
|
||||||
|
"test:database": "node --import tsx --test src/test/DatabaseService.test.ts",
|
||||||
|
"test:search": "node --import tsx --test src/test/SearchService.test.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"better-sqlite3": "^9.4.5",
|
||||||
|
"csv-parse": "^5.5.5",
|
||||||
|
"fast-glob": "^3.3.2",
|
||||||
|
"fuse.js": "^6.6.2",
|
||||||
|
"music-metadata": "^10.5.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"wavefile": "^11.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/better-sqlite3": "^7.6.8",
|
||||||
|
"@types/node": "^20.12.7",
|
||||||
|
"@types/react": "^18.3.3",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.3",
|
||||||
|
"concurrently": "^8.2.2",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"electron": "^29.4.4",
|
||||||
|
"tree-kill": "^1.2.2",
|
||||||
|
"tsx": "^4.15.7",
|
||||||
|
"typescript": "^5.3.3",
|
||||||
|
"vite": "^5.1.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
121
scripts/start-electron.mjs
Normal file
121
scripts/start-electron.mjs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
/** @typedef {import('node:child_process').ChildProcess} ChildProcess */
|
||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import treeKill from 'tree-kill';
|
||||||
|
import electronPath from 'electron';
|
||||||
|
|
||||||
|
const filename = fileURLToPath(import.meta.url);
|
||||||
|
const dirname = path.dirname(filename);
|
||||||
|
const distMainEntry = path.resolve(dirname, '../dist/main/main/index.js');
|
||||||
|
const distMainDir = path.dirname(distMainEntry);
|
||||||
|
let child = /** @type {ChildProcess | null} */ (null);
|
||||||
|
let restartTimer = /** @type {NodeJS.Timeout | null} */ (null);
|
||||||
|
let isStarting = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple debounce helper to avoid rapid restarts.
|
||||||
|
* @param {() => void} fn
|
||||||
|
* @param {number} delayMs
|
||||||
|
*/
|
||||||
|
function schedule(fn, delayMs) {
|
||||||
|
if (restartTimer) {
|
||||||
|
clearTimeout(restartTimer);
|
||||||
|
}
|
||||||
|
restartTimer = setTimeout(() => {
|
||||||
|
restartTimer = null;
|
||||||
|
fn();
|
||||||
|
}, delayMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits until the compiled main bundle exists before attempting to launch Electron.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function waitForBundle() {
|
||||||
|
if (fs.existsSync(distMainEntry)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (fs.existsSync(distMainEntry)) {
|
||||||
|
clearInterval(interval);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
}, 250);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts Electron pointing at the compiled JavaScript main entry.
|
||||||
|
*/
|
||||||
|
function launchElectron() {
|
||||||
|
if (child) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isStarting = true;
|
||||||
|
const spawnOptions = {
|
||||||
|
stdio: 'inherit',
|
||||||
|
env: { ...process.env },
|
||||||
|
windowsHide: false
|
||||||
|
};
|
||||||
|
child = spawn(electronPath, [distMainEntry], spawnOptions);
|
||||||
|
|
||||||
|
child.on('close', (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
console.error(`Electron exited with code ${code ?? 'unknown'}`);
|
||||||
|
}
|
||||||
|
child = null;
|
||||||
|
isStarting = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
isStarting = false;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kills the current Electron process if it is running.
|
||||||
|
*/
|
||||||
|
function disposeElectron() {
|
||||||
|
if (child?.pid) {
|
||||||
|
try {
|
||||||
|
treeKill(child.pid, 'SIGTERM');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to terminate Electron process', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
child = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the dev runner by waiting for the bundle, launching Electron, and watching for rebuilds.
|
||||||
|
*/
|
||||||
|
async function bootstrap() {
|
||||||
|
await waitForBundle();
|
||||||
|
launchElectron();
|
||||||
|
|
||||||
|
const watcher = fs.watch(distMainDir, { recursive: true }, (eventType, filename) => {
|
||||||
|
if (!filename || isStarting || child === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (filename.endsWith('.js') || filename.endsWith('.js.map')) {
|
||||||
|
schedule(() => {
|
||||||
|
disposeElectron();
|
||||||
|
launchElectron();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('SIGINT', () => {
|
||||||
|
watcher.close();
|
||||||
|
disposeElectron();
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrap().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
37
scripts/start-electron.ts
Normal file
37
scripts/start-electron.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
/// <reference types="node" />
|
||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const filename = fileURLToPath(import.meta.url);
|
||||||
|
const dirname = path.dirname(filename);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the Electron binary path based on the current platform.
|
||||||
|
* @returns {string} Absolute path to the Electron executable.
|
||||||
|
*/
|
||||||
|
function resolveElectronBinary(): string {
|
||||||
|
const base = path.resolve(dirname, '../node_modules/.bin');
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
return path.join(base, 'electron.cmd');
|
||||||
|
}
|
||||||
|
return path.join(base, 'electron');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Boots Electron pointing at the TypeScript main entry so tsx can transpile on the fly.
|
||||||
|
*/
|
||||||
|
function launchElectron(): void {
|
||||||
|
const electronBinary = resolveElectronBinary();
|
||||||
|
const mainEntry = path.resolve(dirname, '../src/main/index.ts');
|
||||||
|
const child = spawn(electronBinary, [mainEntry], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
env: { ...process.env }
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('close', (code?: number) => {
|
||||||
|
process.exit(code ?? 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
launchElectron();
|
||||||
374
src/main/MainApp.ts
Normal file
374
src/main/MainApp.ts
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { app, BrowserWindow, dialog, ipcMain, nativeTheme, Menu } from 'electron';
|
||||||
|
import type { IpcMainInvokeEvent } from 'electron';
|
||||||
|
import { IPC_CHANNELS } from '../shared/ipc';
|
||||||
|
import { TagUpdatePayload } from '../shared/models';
|
||||||
|
import { DatabaseService } from './services/DatabaseService';
|
||||||
|
import { LibraryService } from './services/LibraryService';
|
||||||
|
import { SearchService } from './services/SearchService';
|
||||||
|
import { SettingsService } from './services/SettingsService';
|
||||||
|
import { TagService } from './services/TagService';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Central application coordinator responsible for bootstrapping Electron and wiring IPC handlers.
|
||||||
|
*/
|
||||||
|
export class MainApp {
|
||||||
|
private mainWindow: BrowserWindow | null = null;
|
||||||
|
private database: DatabaseService | null = null;
|
||||||
|
private settingsService: SettingsService | null = null;
|
||||||
|
private libraryService: LibraryService | null = null;
|
||||||
|
private searchService: SearchService | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Entry point called once Electron is ready.
|
||||||
|
*/
|
||||||
|
public async initialize(): Promise<void> {
|
||||||
|
nativeTheme.themeSource = 'dark';
|
||||||
|
const userData = app.getPath('userData');
|
||||||
|
const dbPath = path.join(userData, 'audiosort', 'audiosort.db');
|
||||||
|
|
||||||
|
this.database = new DatabaseService(dbPath);
|
||||||
|
this.database.initialize();
|
||||||
|
|
||||||
|
this.settingsService = new SettingsService(this.database);
|
||||||
|
const tagService = new TagService(this.database);
|
||||||
|
this.searchService = new SearchService(this.database, tagService);
|
||||||
|
this.libraryService = new LibraryService(
|
||||||
|
this.database,
|
||||||
|
this.settingsService,
|
||||||
|
tagService,
|
||||||
|
this.searchService
|
||||||
|
);
|
||||||
|
|
||||||
|
const catalogPath = this.resolveResourcePath('UCS.csv');
|
||||||
|
await this.libraryService.ensureCategoriesLoaded(catalogPath);
|
||||||
|
await this.libraryService.scanLibrary();
|
||||||
|
|
||||||
|
this.searchService.rebuildIndex();
|
||||||
|
this.registerIpcHandlers();
|
||||||
|
this.createMenu();
|
||||||
|
this.createWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the application menu.
|
||||||
|
*/
|
||||||
|
private createMenu(): void {
|
||||||
|
const isMac = process.platform === 'darwin';
|
||||||
|
|
||||||
|
const template: Electron.MenuItemConstructorOptions[] = [
|
||||||
|
...(isMac ? [{
|
||||||
|
label: app.name,
|
||||||
|
submenu: [
|
||||||
|
{ role: 'about' as const },
|
||||||
|
{ type: 'separator' as const },
|
||||||
|
{
|
||||||
|
label: 'Settings',
|
||||||
|
accelerator: 'Cmd+,',
|
||||||
|
click: () => this.mainWindow?.webContents.send('open-settings')
|
||||||
|
},
|
||||||
|
{ type: 'separator' as const },
|
||||||
|
{ role: 'hide' as const },
|
||||||
|
{ role: 'hideOthers' as const },
|
||||||
|
{ role: 'unhide' as const },
|
||||||
|
{ type: 'separator' as const },
|
||||||
|
{ role: 'quit' as const }
|
||||||
|
]
|
||||||
|
}] : []),
|
||||||
|
{
|
||||||
|
label: 'File',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Rescan Library',
|
||||||
|
accelerator: isMac ? 'Cmd+R' : 'Ctrl+R',
|
||||||
|
click: () => this.mainWindow?.webContents.send('rescan-library')
|
||||||
|
},
|
||||||
|
{ type: 'separator' as const },
|
||||||
|
...(!isMac ? [
|
||||||
|
{
|
||||||
|
label: 'Settings',
|
||||||
|
accelerator: 'Ctrl+,',
|
||||||
|
click: () => this.mainWindow?.webContents.send('open-settings')
|
||||||
|
},
|
||||||
|
{ type: 'separator' as const }
|
||||||
|
] : []),
|
||||||
|
...(isMac ? [{ role: 'close' as const }] : [{ role: 'quit' as const }])
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Tools',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'Find Duplicates',
|
||||||
|
click: () => this.mainWindow?.webContents.send('find-duplicates')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'View',
|
||||||
|
submenu: [
|
||||||
|
{ role: 'reload' as const },
|
||||||
|
{ role: 'forceReload' as const },
|
||||||
|
{ role: 'toggleDevTools' as const },
|
||||||
|
{ type: 'separator' as const },
|
||||||
|
{ role: 'resetZoom' as const },
|
||||||
|
{ role: 'zoomIn' as const },
|
||||||
|
{ role: 'zoomOut' as const },
|
||||||
|
{ type: 'separator' as const },
|
||||||
|
{ role: 'togglefullscreen' as const }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const menu = Menu.buildFromTemplate(template);
|
||||||
|
Menu.setApplicationMenu(menu);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gracefully releases resources during application shutdown.
|
||||||
|
*/
|
||||||
|
public dispose(): void {
|
||||||
|
ipcMain.removeHandler(IPC_CHANNELS.settingsGet);
|
||||||
|
ipcMain.removeHandler(IPC_CHANNELS.settingsSetLibrary);
|
||||||
|
ipcMain.removeHandler(IPC_CHANNELS.dialogSelectLibrary);
|
||||||
|
ipcMain.removeHandler(IPC_CHANNELS.libraryScan);
|
||||||
|
ipcMain.removeHandler(IPC_CHANNELS.libraryList);
|
||||||
|
ipcMain.removeHandler(IPC_CHANNELS.libraryRename);
|
||||||
|
ipcMain.removeHandler(IPC_CHANNELS.libraryMove);
|
||||||
|
ipcMain.removeHandler(IPC_CHANNELS.libraryOrganize);
|
||||||
|
ipcMain.removeHandler(IPC_CHANNELS.libraryBuffer);
|
||||||
|
ipcMain.removeHandler(IPC_CHANNELS.libraryMetadata);
|
||||||
|
ipcMain.removeHandler(IPC_CHANNELS.libraryMetadataSuggestions);
|
||||||
|
ipcMain.removeHandler(IPC_CHANNELS.libraryUpdateMetadata);
|
||||||
|
ipcMain.removeHandler(IPC_CHANNELS.libraryWaveformPreview);
|
||||||
|
ipcMain.removeHandler(IPC_CHANNELS.tagsUpdate);
|
||||||
|
ipcMain.removeHandler(IPC_CHANNELS.categoriesList);
|
||||||
|
ipcMain.removeHandler(IPC_CHANNELS.searchQuery);
|
||||||
|
this.database?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reopens the renderer window when requested (for macOS style activate behaviour).
|
||||||
|
*/
|
||||||
|
public ensureWindow(): void {
|
||||||
|
if (this.mainWindow) {
|
||||||
|
this.mainWindow.focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.createWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the renderer window and loads the UI.
|
||||||
|
*/
|
||||||
|
private createWindow(): void {
|
||||||
|
const preloadPath = this.resolvePreloadPath();
|
||||||
|
this.mainWindow = new BrowserWindow({
|
||||||
|
width: 1280,
|
||||||
|
height: 800,
|
||||||
|
backgroundColor: '#111518',
|
||||||
|
show: false,
|
||||||
|
title: 'AudioSort',
|
||||||
|
webPreferences: {
|
||||||
|
preload: preloadPath,
|
||||||
|
nodeIntegration: false,
|
||||||
|
contextIsolation: true,
|
||||||
|
sandbox: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.mainWindow.on('ready-to-show', () => {
|
||||||
|
this.mainWindow?.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.mainWindow.on('closed', () => {
|
||||||
|
this.mainWindow = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const devServerUrl = (globalThis as { process?: { env?: Record<string, string | undefined> } }).process?.env
|
||||||
|
?.VITE_DEV_SERVER_URL;
|
||||||
|
if (devServerUrl) {
|
||||||
|
this.mainWindow.loadURL(devServerUrl).catch((error: unknown) => {
|
||||||
|
// eslint-disable-next-line no-console -- Logging is useful during development to diagnose boot issues.
|
||||||
|
console.error('Failed to load renderer URL', error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const rendererIndex = this.resolveRendererIndex();
|
||||||
|
this.mainWindow
|
||||||
|
.loadFile(rendererIndex)
|
||||||
|
.catch((error: unknown) => console.error('Failed to load renderer bundle', error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wires IPC handlers that power the renderer bridge API.
|
||||||
|
*/
|
||||||
|
private registerIpcHandlers(): void {
|
||||||
|
ipcMain.handle(IPC_CHANNELS.settingsGet, async () => this.requireSettings().getSettings());
|
||||||
|
|
||||||
|
ipcMain.handle(IPC_CHANNELS.settingsSetLibrary, async (_event: IpcMainInvokeEvent, targetPath: string) => {
|
||||||
|
const settings = this.requireSettings().updateLibraryPath(targetPath);
|
||||||
|
await this.requireLibrary().scanLibrary();
|
||||||
|
return settings;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle(IPC_CHANNELS.dialogSelectLibrary, async () => {
|
||||||
|
const options: Electron.OpenDialogOptions = { properties: ['openDirectory'] };
|
||||||
|
const targetWindow = this.mainWindow;
|
||||||
|
const result = targetWindow
|
||||||
|
? await dialog.showOpenDialog(targetWindow, options)
|
||||||
|
: await dialog.showOpenDialog(options);
|
||||||
|
return result.canceled ? null : result.filePaths[0] ?? null;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle(IPC_CHANNELS.libraryScan, async () => this.requireLibrary().scanLibrary());
|
||||||
|
ipcMain.handle(IPC_CHANNELS.libraryList, async () => this.requireLibrary().listFiles());
|
||||||
|
ipcMain.handle(IPC_CHANNELS.libraryDuplicates, async () => this.requireLibrary().listDuplicates());
|
||||||
|
ipcMain.handle(IPC_CHANNELS.searchQuery, async (_event: IpcMainInvokeEvent, query: string) =>
|
||||||
|
this.requireSearch().search(query)
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcMain.handle(IPC_CHANNELS.libraryRename, async (_event: IpcMainInvokeEvent, fileId: number, newName: string) =>
|
||||||
|
this.requireLibrary().renameFile(fileId, newName)
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcMain.handle(
|
||||||
|
IPC_CHANNELS.libraryMove,
|
||||||
|
async (_event: IpcMainInvokeEvent, fileId: number, targetRelativeDirectory: string) =>
|
||||||
|
this.requireLibrary().moveFile(fileId, targetRelativeDirectory)
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcMain.handle(
|
||||||
|
IPC_CHANNELS.libraryOrganize,
|
||||||
|
async (_event: IpcMainInvokeEvent, fileId: number, metadata: { customName?: string | null; author?: string | null; copyright?: string | null; rating?: number }) =>
|
||||||
|
this.requireLibrary().organizeFile(fileId, metadata)
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcMain.handle(
|
||||||
|
IPC_CHANNELS.libraryCustomName,
|
||||||
|
async (_event: IpcMainInvokeEvent, fileId: number, customName: string | null) =>
|
||||||
|
this.requireLibrary().updateCustomName(fileId, customName)
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcMain.handle(IPC_CHANNELS.libraryOpenFolder, async (_event: IpcMainInvokeEvent, fileId: number) =>
|
||||||
|
this.requireLibrary().openFileFolder(fileId)
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcMain.handle(IPC_CHANNELS.libraryDelete, async (_event: IpcMainInvokeEvent, fileIds: number[]) =>
|
||||||
|
this.requireLibrary().deleteFiles(fileIds)
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcMain.handle(IPC_CHANNELS.libraryBuffer, async (_event: IpcMainInvokeEvent, fileId: number) =>
|
||||||
|
this.requireLibrary().getAudioBuffer(fileId)
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcMain.handle(
|
||||||
|
IPC_CHANNELS.libraryWaveformPreview,
|
||||||
|
async (_event: IpcMainInvokeEvent, fileId: number, pointCount: number | undefined) =>
|
||||||
|
this.requireLibrary().getWaveformPreview(fileId, pointCount)
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcMain.handle(IPC_CHANNELS.tagsUpdate, async (_event: IpcMainInvokeEvent, payload: TagUpdatePayload) =>
|
||||||
|
this.requireLibrary().updateTagging(payload)
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcMain.handle(IPC_CHANNELS.categoriesList, async () => this.requireLibrary().listCategories());
|
||||||
|
|
||||||
|
ipcMain.handle(IPC_CHANNELS.libraryMetadata, async (_event: IpcMainInvokeEvent, fileId: number) =>
|
||||||
|
this.requireLibrary().readFileMetadata(fileId)
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcMain.handle(IPC_CHANNELS.libraryMetadataSuggestions, async () =>
|
||||||
|
this.requireLibrary().listMetadataSuggestions()
|
||||||
|
);
|
||||||
|
|
||||||
|
ipcMain.handle(
|
||||||
|
IPC_CHANNELS.libraryUpdateMetadata,
|
||||||
|
async (_event: IpcMainInvokeEvent, fileId: number, metadata: { author?: string | null; copyright?: string | null; rating?: number }) =>
|
||||||
|
this.requireLibrary().updateFileMetadata(fileId, metadata)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves resources that may live either beside the compiled output or in the project root in development.
|
||||||
|
*/
|
||||||
|
private resolveResourcePath(relativePath: string): string {
|
||||||
|
for (const base of this.buildSearchBases()) {
|
||||||
|
const candidate = path.resolve(base, relativePath);
|
||||||
|
if (fs.existsSync(candidate)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`Resource ${relativePath} could not be located.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolvePreloadPath(): string {
|
||||||
|
const relativeCandidates = [
|
||||||
|
'dist/main/preload/index.js',
|
||||||
|
'main/preload/index.js',
|
||||||
|
'preload/index.js',
|
||||||
|
'src/preload/index.js'
|
||||||
|
];
|
||||||
|
for (const base of this.buildSearchBases()) {
|
||||||
|
for (const relative of relativeCandidates) {
|
||||||
|
const candidate = path.resolve(base, relative);
|
||||||
|
if (fs.existsSync(candidate)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('Unable to resolve preload script.');
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveRendererIndex(): string {
|
||||||
|
const relativeCandidates = ['dist/renderer/index.html', 'renderer/index.html', 'src/renderer/index.html'];
|
||||||
|
for (const base of this.buildSearchBases()) {
|
||||||
|
for (const relative of relativeCandidates) {
|
||||||
|
const candidate = path.resolve(base, relative);
|
||||||
|
if (fs.existsSync(candidate)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('Renderer bundle not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveCwdFallback(): string {
|
||||||
|
const processRef = (globalThis as { process?: { cwd?: () => string } }).process;
|
||||||
|
if (processRef?.cwd) {
|
||||||
|
return processRef.cwd();
|
||||||
|
}
|
||||||
|
return app.getAppPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildSearchBases(): string[] {
|
||||||
|
const bases = new Set<string>();
|
||||||
|
const appPath = app.getAppPath();
|
||||||
|
bases.add(appPath);
|
||||||
|
bases.add(path.resolve(appPath, '..'));
|
||||||
|
bases.add(this.resolveCwdFallback());
|
||||||
|
return Array.from(bases);
|
||||||
|
}
|
||||||
|
|
||||||
|
private requireSettings(): SettingsService {
|
||||||
|
if (!this.settingsService) {
|
||||||
|
throw new Error('SettingsService has not been initialised.');
|
||||||
|
}
|
||||||
|
return this.settingsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
private requireLibrary(): LibraryService {
|
||||||
|
if (!this.libraryService) {
|
||||||
|
throw new Error('LibraryService has not been initialised.');
|
||||||
|
}
|
||||||
|
return this.libraryService;
|
||||||
|
}
|
||||||
|
|
||||||
|
private requireSearch(): SearchService {
|
||||||
|
if (!this.searchService) {
|
||||||
|
throw new Error('SearchService has not been initialised.');
|
||||||
|
}
|
||||||
|
return this.searchService;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/main/index.ts
Normal file
27
src/main/index.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { app } from 'electron';
|
||||||
|
import { MainApp } from './MainApp';
|
||||||
|
|
||||||
|
const mainApp = new MainApp();
|
||||||
|
|
||||||
|
app.whenReady()
|
||||||
|
.then(() => mainApp.initialize())
|
||||||
|
.catch((error: unknown) => {
|
||||||
|
// eslint-disable-next-line no-console -- Logging critical boot failures is necessary for troubleshooting.
|
||||||
|
console.error('Failed to initialise application', error);
|
||||||
|
app.quit();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('window-all-closed', () => {
|
||||||
|
const processRef = (globalThis as { process?: { platform?: string } }).process;
|
||||||
|
if (processRef?.platform !== 'darwin') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('activate', () => {
|
||||||
|
mainApp.ensureWindow();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('before-quit', () => {
|
||||||
|
mainApp.dispose();
|
||||||
|
});
|
||||||
507
src/main/services/DatabaseService.ts
Normal file
507
src/main/services/DatabaseService.ts
Normal file
@@ -0,0 +1,507 @@
|
|||||||
|
/**
|
||||||
|
* Thin wrapper around better-sqlite3 providing schema setup and helpers used by the main process services.
|
||||||
|
*/
|
||||||
|
import Database, { Database as BetterSqliteDatabase } from 'better-sqlite3';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { AppSettings, AudioFileSummary, CategoryRecord } from '../../shared/models';
|
||||||
|
|
||||||
|
export interface FileRecordInput {
|
||||||
|
/** Absolute filesystem path. */
|
||||||
|
absolutePath: string;
|
||||||
|
/** Relative path inside the library root. */
|
||||||
|
relativePath: string;
|
||||||
|
/** Filename including extension. */
|
||||||
|
fileName: string;
|
||||||
|
/** Display name without extension. */
|
||||||
|
displayName: string;
|
||||||
|
/** Unix epoch milliseconds. */
|
||||||
|
modifiedAt: number;
|
||||||
|
/** File creation time if available. */
|
||||||
|
createdAt: number | null;
|
||||||
|
/** File size in bytes. */
|
||||||
|
size: number;
|
||||||
|
/** Duration in milliseconds if known. */
|
||||||
|
durationMs: number | null;
|
||||||
|
/** Sample rate in Hz if known. */
|
||||||
|
sampleRate: number | null;
|
||||||
|
/** Bit depth if known. */
|
||||||
|
bitDepth: number | null;
|
||||||
|
/** MD5 checksum of the file contents. */
|
||||||
|
checksum: string | null;
|
||||||
|
/** Optional tag payload (stored as JSON string). */
|
||||||
|
tags?: string[];
|
||||||
|
/** Optional category payload (stored as JSON string). */
|
||||||
|
categories?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileRecordRow extends AudioFileSummary {}
|
||||||
|
|
||||||
|
type DbRow = Record<string, unknown>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles persistence for files, categories, and settings.
|
||||||
|
*/
|
||||||
|
export class DatabaseService {
|
||||||
|
private db: BetterSqliteDatabase | null = null;
|
||||||
|
|
||||||
|
public constructor(private readonly dbFilePath: string) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the database connection (creating the file if necessary) and ensures the schema exists.
|
||||||
|
*/
|
||||||
|
public initialize(): void {
|
||||||
|
const folder = path.dirname(this.dbFilePath);
|
||||||
|
if (!fs.existsSync(folder)) {
|
||||||
|
fs.mkdirSync(folder, { recursive: true });
|
||||||
|
}
|
||||||
|
this.db = new Database(this.dbFilePath);
|
||||||
|
this.db.pragma('journal_mode = WAL');
|
||||||
|
this.db.pragma('foreign_keys = ON');
|
||||||
|
this.applySchema();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the active database connection.
|
||||||
|
*/
|
||||||
|
public close(): void {
|
||||||
|
this.db?.close();
|
||||||
|
this.db = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists or updates a file row and returns the stored record.
|
||||||
|
* @param record File description to persist.
|
||||||
|
*/
|
||||||
|
public upsertFile(record: FileRecordInput): FileRecordRow {
|
||||||
|
const connection = this.requireDb();
|
||||||
|
const statement = connection.prepare(
|
||||||
|
`INSERT INTO files (
|
||||||
|
absolute_path,
|
||||||
|
library_relative_path,
|
||||||
|
file_name,
|
||||||
|
display_name,
|
||||||
|
modified_at,
|
||||||
|
created_at,
|
||||||
|
size_bytes,
|
||||||
|
duration_ms,
|
||||||
|
sample_rate,
|
||||||
|
bit_depth,
|
||||||
|
checksum,
|
||||||
|
tags_json,
|
||||||
|
categories_json
|
||||||
|
) VALUES (
|
||||||
|
@absolutePath,
|
||||||
|
@relativePath,
|
||||||
|
@fileName,
|
||||||
|
@displayName,
|
||||||
|
@modifiedAt,
|
||||||
|
@createdAt,
|
||||||
|
@size,
|
||||||
|
@durationMs,
|
||||||
|
@sampleRate,
|
||||||
|
@bitDepth,
|
||||||
|
@checksum,
|
||||||
|
@tagsJson,
|
||||||
|
@categoriesJson
|
||||||
|
)
|
||||||
|
ON CONFLICT(absolute_path) DO UPDATE SET
|
||||||
|
library_relative_path = excluded.library_relative_path,
|
||||||
|
file_name = excluded.file_name,
|
||||||
|
display_name = excluded.display_name,
|
||||||
|
modified_at = excluded.modified_at,
|
||||||
|
created_at = COALESCE(files.created_at, excluded.created_at),
|
||||||
|
size_bytes = excluded.size_bytes,
|
||||||
|
duration_ms = excluded.duration_ms,
|
||||||
|
sample_rate = excluded.sample_rate,
|
||||||
|
bit_depth = excluded.bit_depth,
|
||||||
|
checksum = excluded.checksum,
|
||||||
|
tags_json = CASE WHEN files.tags_json = '[]' THEN excluded.tags_json ELSE files.tags_json END,
|
||||||
|
categories_json = CASE WHEN files.categories_json = '[]' THEN excluded.categories_json ELSE files.categories_json END
|
||||||
|
RETURNING *`
|
||||||
|
);
|
||||||
|
|
||||||
|
const row = statement.get({
|
||||||
|
absolutePath: record.absolutePath,
|
||||||
|
relativePath: record.relativePath,
|
||||||
|
fileName: record.fileName,
|
||||||
|
displayName: record.displayName,
|
||||||
|
modifiedAt: record.modifiedAt,
|
||||||
|
createdAt: record.createdAt,
|
||||||
|
size: record.size,
|
||||||
|
durationMs: record.durationMs,
|
||||||
|
sampleRate: record.sampleRate,
|
||||||
|
bitDepth: record.bitDepth,
|
||||||
|
checksum: record.checksum,
|
||||||
|
tagsJson: JSON.stringify(record.tags ?? []),
|
||||||
|
categoriesJson: JSON.stringify(record.categories ?? [])
|
||||||
|
}) as DbRow | undefined;
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
throw new Error('Failed to persist file record.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.mapFileRow(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the stored tags and categories for a file.
|
||||||
|
*/
|
||||||
|
public updateTagging(fileId: number, tags: string[], categories: string[]): AudioFileSummary {
|
||||||
|
const connection = this.requireDb();
|
||||||
|
const statement = connection.prepare(
|
||||||
|
`UPDATE files
|
||||||
|
SET tags_json = @tags,
|
||||||
|
categories_json = @categories
|
||||||
|
WHERE id = @id
|
||||||
|
RETURNING *`
|
||||||
|
);
|
||||||
|
const row = statement.get({
|
||||||
|
id: fileId,
|
||||||
|
tags: JSON.stringify(tags),
|
||||||
|
categories: JSON.stringify(categories)
|
||||||
|
}) as DbRow | undefined;
|
||||||
|
if (!row) {
|
||||||
|
throw new Error(`File with id ${fileId} not found`);
|
||||||
|
}
|
||||||
|
return this.mapFileRow(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the custom name for a file.
|
||||||
|
*/
|
||||||
|
public updateCustomName(fileId: number, customName: string | null): AudioFileSummary {
|
||||||
|
const connection = this.requireDb();
|
||||||
|
const statement = connection.prepare(
|
||||||
|
`UPDATE files
|
||||||
|
SET custom_name = @customName
|
||||||
|
WHERE id = @id
|
||||||
|
RETURNING *`
|
||||||
|
);
|
||||||
|
const row = statement.get({
|
||||||
|
id: fileId,
|
||||||
|
customName
|
||||||
|
}) as DbRow | undefined;
|
||||||
|
if (!row) {
|
||||||
|
throw new Error(`File with id ${fileId} not found`);
|
||||||
|
}
|
||||||
|
return this.mapFileRow(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a single file row by id.
|
||||||
|
*/
|
||||||
|
public getFileById(fileId: number): AudioFileSummary {
|
||||||
|
const row = this.requireDb()
|
||||||
|
.prepare('SELECT * FROM files WHERE id = ?')
|
||||||
|
.get(fileId) as DbRow | undefined;
|
||||||
|
if (!row) {
|
||||||
|
throw new Error(`File with id ${fileId} not found`);
|
||||||
|
}
|
||||||
|
return this.mapFileRow(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists all audio files currently known to the database.
|
||||||
|
*/
|
||||||
|
public listFiles(): AudioFileSummary[] {
|
||||||
|
const rows = this.requireDb()
|
||||||
|
.prepare('SELECT * FROM files ORDER BY display_name COLLATE NOCASE ASC')
|
||||||
|
.all() as DbRow[];
|
||||||
|
return rows.map((row) => this.mapFileRow(row));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a file record permanently.
|
||||||
|
*/
|
||||||
|
public deleteFile(fileId: number): void {
|
||||||
|
const statement = this.requireDb().prepare('DELETE FROM files WHERE id = ?');
|
||||||
|
statement.run(fileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns groups of files that share the same checksum.
|
||||||
|
*/
|
||||||
|
public listDuplicateGroups(): { checksum: string; files: AudioFileSummary[] }[] {
|
||||||
|
const connection = this.requireDb();
|
||||||
|
const checksumRows = connection
|
||||||
|
.prepare(`
|
||||||
|
SELECT checksum
|
||||||
|
FROM files
|
||||||
|
WHERE checksum IS NOT NULL AND checksum <> ''
|
||||||
|
GROUP BY checksum
|
||||||
|
HAVING COUNT(*) > 1
|
||||||
|
`)
|
||||||
|
.all() as Array<{ checksum: string }>;
|
||||||
|
|
||||||
|
const fileQuery = connection.prepare('SELECT * FROM files WHERE checksum = ? ORDER BY created_at ASC, modified_at ASC');
|
||||||
|
return checksumRows.map((row) => ({
|
||||||
|
checksum: row.checksum,
|
||||||
|
files: (fileQuery.all(row.checksum) as DbRow[]).map((fileRow) => this.mapFileRow(fileRow))
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes file records whose absolute path is not part of the provided set.
|
||||||
|
* @param validPathsSet Set of absolute paths that should remain in the database.
|
||||||
|
* @returns Number of removed rows.
|
||||||
|
*/
|
||||||
|
public removeFilesOutside(validPathsSet: Set<string>): number {
|
||||||
|
const connection = this.requireDb();
|
||||||
|
const rows = connection.prepare('SELECT id, absolute_path FROM files').all() as DbRow[];
|
||||||
|
let removed = 0;
|
||||||
|
const deleteStatement = connection.prepare('DELETE FROM files WHERE id = ?');
|
||||||
|
for (const row of rows) {
|
||||||
|
const absolutePath = row.absolute_path as string | undefined;
|
||||||
|
const id = row.id as number | undefined;
|
||||||
|
if (!absolutePath || typeof id !== 'number') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!validPathsSet.has(absolutePath)) {
|
||||||
|
deleteStatement.run(id);
|
||||||
|
removed += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates file path and naming metadata after a rename or move operation.
|
||||||
|
*/
|
||||||
|
public updateFileLocation(
|
||||||
|
fileId: number,
|
||||||
|
absolutePath: string,
|
||||||
|
relativePath: string,
|
||||||
|
fileName: string,
|
||||||
|
displayName: string
|
||||||
|
): AudioFileSummary {
|
||||||
|
const row = this.requireDb()
|
||||||
|
.prepare(
|
||||||
|
`UPDATE files
|
||||||
|
SET absolute_path = @absolutePath,
|
||||||
|
library_relative_path = @relativePath,
|
||||||
|
file_name = @fileName,
|
||||||
|
display_name = @displayName
|
||||||
|
WHERE id = @id
|
||||||
|
RETURNING *`
|
||||||
|
)
|
||||||
|
.get({ id: fileId, absolutePath, relativePath, fileName, displayName }) as DbRow | undefined;
|
||||||
|
if (!row) {
|
||||||
|
throw new Error(`File with id ${fileId} not found`);
|
||||||
|
}
|
||||||
|
return this.mapFileRow(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves application settings as a typed object.
|
||||||
|
*/
|
||||||
|
public getSettings(): AppSettings {
|
||||||
|
const connection = this.requireDb();
|
||||||
|
const rows = connection.prepare('SELECT key, value FROM settings').all() as DbRow[];
|
||||||
|
const map = new Map<string, string>();
|
||||||
|
for (const row of rows) {
|
||||||
|
const key = row.key as string | undefined;
|
||||||
|
const value = row.value as string | undefined;
|
||||||
|
if (key && typeof value === 'string') {
|
||||||
|
map.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
libraryPath: map.has('libraryPath') ? (JSON.parse(map.get('libraryPath') as string) as string) : null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists a single setting key.
|
||||||
|
*/
|
||||||
|
public setSetting(key: string, value: unknown): void {
|
||||||
|
this.requireDb()
|
||||||
|
.prepare(
|
||||||
|
`INSERT INTO settings (key, value) VALUES (@key, @value)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET value = excluded.value`
|
||||||
|
)
|
||||||
|
.run({ key, value: JSON.stringify(value) });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts or updates a UCS category record.
|
||||||
|
*/
|
||||||
|
public upsertCategory(category: CategoryRecord): void {
|
||||||
|
this.requireDb()
|
||||||
|
.prepare(
|
||||||
|
`INSERT INTO categories (
|
||||||
|
id,
|
||||||
|
category,
|
||||||
|
sub_category,
|
||||||
|
short_code,
|
||||||
|
explanation,
|
||||||
|
synonyms_json
|
||||||
|
) VALUES (@id, @category, @subCategory, @shortCode, @explanation, @synonyms)
|
||||||
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
|
category = excluded.category,
|
||||||
|
sub_category = excluded.sub_category,
|
||||||
|
short_code = excluded.short_code,
|
||||||
|
explanation = excluded.explanation,
|
||||||
|
synonyms_json = excluded.synonyms_json`
|
||||||
|
)
|
||||||
|
.run({
|
||||||
|
id: category.id,
|
||||||
|
category: category.category,
|
||||||
|
subCategory: category.subCategory,
|
||||||
|
shortCode: category.shortCode,
|
||||||
|
explanation: category.explanation,
|
||||||
|
synonyms: JSON.stringify(category.synonyms)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the full catalog of UCS categories.
|
||||||
|
*/
|
||||||
|
public listCategories(): CategoryRecord[] {
|
||||||
|
const rows = this.requireDb()
|
||||||
|
.prepare('SELECT * FROM categories ORDER BY category, sub_category')
|
||||||
|
.all() as DbRow[];
|
||||||
|
return rows.map((row) => ({
|
||||||
|
id: row.id as string,
|
||||||
|
category: row.category as string,
|
||||||
|
subCategory: row.sub_category as string,
|
||||||
|
shortCode: row.short_code as string,
|
||||||
|
explanation: row.explanation as string,
|
||||||
|
synonyms: this.parseJsonArray(row.synonyms_json)
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a category by its CatID.
|
||||||
|
*/
|
||||||
|
public getCategoryById(catId: string): CategoryRecord | null {
|
||||||
|
const row = this.requireDb()
|
||||||
|
.prepare('SELECT * FROM categories WHERE id = ?')
|
||||||
|
.get(catId) as DbRow | undefined;
|
||||||
|
if (!row) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: row.id as string,
|
||||||
|
category: row.category as string,
|
||||||
|
subCategory: row.sub_category as string,
|
||||||
|
shortCode: row.short_code as string,
|
||||||
|
explanation: row.explanation as string,
|
||||||
|
synonyms: this.parseJsonArray(row.synonyms_json)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps a raw database row to the strongly typed summary shape.
|
||||||
|
*/
|
||||||
|
private mapFileRow(row: DbRow): AudioFileSummary {
|
||||||
|
return {
|
||||||
|
id: row.id as number,
|
||||||
|
absolutePath: row.absolute_path as string,
|
||||||
|
relativePath: row.library_relative_path as string,
|
||||||
|
fileName: row.file_name as string,
|
||||||
|
displayName: row.display_name as string,
|
||||||
|
modifiedAt: row.modified_at as number,
|
||||||
|
createdAt: row.created_at === null || row.created_at === undefined ? null : (row.created_at as number),
|
||||||
|
size: row.size_bytes as number,
|
||||||
|
durationMs: row.duration_ms === null ? null : (row.duration_ms as number),
|
||||||
|
sampleRate: row.sample_rate === null ? null : (row.sample_rate as number),
|
||||||
|
bitDepth: row.bit_depth === null ? null : (row.bit_depth as number),
|
||||||
|
checksum: typeof row.checksum === 'string' ? (row.checksum as string) : null,
|
||||||
|
tags: this.parseJsonArray(row.tags_json),
|
||||||
|
categories: this.parseJsonArray(row.categories_json),
|
||||||
|
customName: typeof row.custom_name === 'string' ? (row.custom_name as string) : null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazy accessor ensuring the database has been initialised.
|
||||||
|
*/
|
||||||
|
private requireDb(): BetterSqliteDatabase {
|
||||||
|
if (!this.db) {
|
||||||
|
throw new Error('Database connection has not been initialised.');
|
||||||
|
}
|
||||||
|
return this.db;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies the initial schema for the application.
|
||||||
|
*/
|
||||||
|
private applySchema(): void {
|
||||||
|
const connection = this.requireDb();
|
||||||
|
connection.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS files (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
absolute_path TEXT NOT NULL UNIQUE,
|
||||||
|
library_relative_path TEXT NOT NULL,
|
||||||
|
file_name TEXT NOT NULL,
|
||||||
|
display_name TEXT NOT NULL,
|
||||||
|
modified_at INTEGER NOT NULL,
|
||||||
|
created_at INTEGER,
|
||||||
|
size_bytes INTEGER NOT NULL,
|
||||||
|
duration_ms INTEGER,
|
||||||
|
sample_rate INTEGER,
|
||||||
|
bit_depth INTEGER,
|
||||||
|
checksum TEXT,
|
||||||
|
tags_json TEXT NOT NULL DEFAULT '[]',
|
||||||
|
categories_json TEXT NOT NULL DEFAULT '[]'
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_files_display_name ON files(display_name);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_files_modified ON files(modified_at);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
key TEXT PRIMARY KEY,
|
||||||
|
value TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS categories (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
category TEXT NOT NULL,
|
||||||
|
sub_category TEXT NOT NULL,
|
||||||
|
short_code TEXT NOT NULL,
|
||||||
|
explanation TEXT NOT NULL,
|
||||||
|
synonyms_json TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS file_operations (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
file_id INTEGER NOT NULL,
|
||||||
|
previous_path TEXT NOT NULL,
|
||||||
|
new_path TEXT NOT NULL,
|
||||||
|
operation_type TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Add newly introduced columns if the table already existed.
|
||||||
|
this.addColumnIfMissing(connection, 'files', 'created_at', 'INTEGER');
|
||||||
|
this.addColumnIfMissing(connection, 'files', 'checksum', 'TEXT');
|
||||||
|
this.addColumnIfMissing(connection, 'files', 'custom_name', 'TEXT');
|
||||||
|
|
||||||
|
// Create checksum index after ensuring column exists
|
||||||
|
connection.exec('CREATE INDEX IF NOT EXISTS idx_files_checksum ON files(checksum)');
|
||||||
|
}
|
||||||
|
|
||||||
|
private addColumnIfMissing(connection: BetterSqliteDatabase, table: string, column: string, definition: string): void {
|
||||||
|
const info = connection.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
|
||||||
|
if (!info.some((row) => row.name === column)) {
|
||||||
|
connection.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely parses stored JSON arrays, handling legacy nulls or malformed values.
|
||||||
|
*/
|
||||||
|
private parseJsonArray(value: unknown): string[] {
|
||||||
|
if (typeof value !== 'string' || value.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(value) as unknown;
|
||||||
|
return Array.isArray(parsed) ? parsed.map((entry) => String(entry)) : [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1237
src/main/services/LibraryService.ts
Normal file
1237
src/main/services/LibraryService.ts
Normal file
File diff suppressed because it is too large
Load Diff
127
src/main/services/OrganizationService.ts
Normal file
127
src/main/services/OrganizationService.ts
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import path from 'node:path';
|
||||||
|
import { AudioFileSummary, CategoryRecord } from '../../shared/models';
|
||||||
|
import { DatabaseService } from './DatabaseService';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles automatic file organization based on UCS categories.
|
||||||
|
* Provides helpers for folder resolution, base name creation, and duplicate handling.
|
||||||
|
*/
|
||||||
|
export class OrganizationService {
|
||||||
|
public constructor(private readonly database: DatabaseService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the primary category record for a file, or null if unavailable.
|
||||||
|
*/
|
||||||
|
public getPrimaryCategory(file: AudioFileSummary): CategoryRecord | null {
|
||||||
|
if (file.categories.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const categoryId = file.categories[0];
|
||||||
|
return this.database.getCategoryById(categoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the folder path for the provided category (CATEGORY/SUBCATEGORY).
|
||||||
|
*/
|
||||||
|
public buildFolderPath(category: CategoryRecord): string {
|
||||||
|
const categoryFolder = category.category.toUpperCase().replace(/\s+/g, '_');
|
||||||
|
const subCategoryFolder = category.subCategory.toUpperCase().replace(/\s+/g, '_');
|
||||||
|
return path.join(categoryFolder, subCategoryFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Produces the base name (without extension) used for organizing files.
|
||||||
|
* Combines the short code with the suffix from CatID, plus custom name if provided.
|
||||||
|
* Example: CatID "VEHUtil" + CatShort "VEH" → "VEH_Util"
|
||||||
|
*/
|
||||||
|
public buildBaseName(category: CategoryRecord, customName?: string): string {
|
||||||
|
const shortCode = category.shortCode.toUpperCase();
|
||||||
|
const suffix = this.extractCatIdSuffix(category.id, category.shortCode);
|
||||||
|
|
||||||
|
let baseName = shortCode;
|
||||||
|
if (suffix) {
|
||||||
|
baseName = `${shortCode}_${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customName) {
|
||||||
|
const clean = this.sanitizeCustomName(customName);
|
||||||
|
if (clean) {
|
||||||
|
baseName = `${baseName}_${clean}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the suffix from CatID by removing the CatShort prefix.
|
||||||
|
* Example: extractCatIdSuffix("VEHUtil", "VEH") → "Util"
|
||||||
|
*/
|
||||||
|
private extractCatIdSuffix(catId: string, catShort: string): string {
|
||||||
|
if (!catId.toLowerCase().startsWith(catShort.toLowerCase())) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const suffix = catId.substring(catShort.length);
|
||||||
|
return suffix.charAt(0).toUpperCase() + suffix.slice(1).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists existing files inside the target folder that share the provided base name.
|
||||||
|
*/
|
||||||
|
public listConflictingFiles(folderPath: string, baseName: string): AudioFileSummary[] {
|
||||||
|
const normalizedFolder = this.normalizePath(folderPath);
|
||||||
|
const pattern = new RegExp(`^${baseName}(?:_(\\d+))?\\.wav$`, 'i');
|
||||||
|
return this.database
|
||||||
|
.listFiles()
|
||||||
|
.filter((file) => this.normalizePath(path.dirname(file.relativePath)) === normalizedFolder)
|
||||||
|
.filter((file) => pattern.test(file.fileName));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines the next available sequence number for the given base name within a folder.
|
||||||
|
*/
|
||||||
|
public findNextAvailableNumber(folderPath: string, baseName: string): number {
|
||||||
|
const conflicts = this.listConflictingFiles(folderPath, baseName);
|
||||||
|
if (conflicts.length === 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const pattern = new RegExp(`^${baseName}_(\\d+)\\.wav$`, 'i');
|
||||||
|
const numbers = conflicts
|
||||||
|
.map((file) => {
|
||||||
|
const match = file.fileName.match(pattern);
|
||||||
|
return match ? parseInt(match[1], 10) : 0;
|
||||||
|
})
|
||||||
|
.filter((value) => value > 0);
|
||||||
|
if (numbers.length === 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return Math.max(...numbers) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a sequence number with leading zeros.
|
||||||
|
*/
|
||||||
|
public formatSequenceNumber(index: number): string {
|
||||||
|
return index.toString().padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exposes the sanitiser so the caller can reuse consistent logic.
|
||||||
|
* Capitalizes the first letter of each word and replaces spaces with underscores.
|
||||||
|
*/
|
||||||
|
public sanitizeCustomName(name: string): string {
|
||||||
|
return name
|
||||||
|
.trim()
|
||||||
|
.replace(/\s+/g, '_')
|
||||||
|
.replace(/[^a-zA-Z0-9_-]/g, '_')
|
||||||
|
.replace(/_+/g, '_')
|
||||||
|
.replace(/^_|_$/g, '')
|
||||||
|
.split('_')
|
||||||
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||||
|
.join('_');
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizePath(value: string): string {
|
||||||
|
return value.replace(/\\/g, '/');
|
||||||
|
}
|
||||||
|
}
|
||||||
189
src/main/services/SearchService.ts
Normal file
189
src/main/services/SearchService.ts
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import Fuse from 'fuse.js';
|
||||||
|
import { AudioFileSummary } from '../../shared/models';
|
||||||
|
import { DatabaseService } from './DatabaseService';
|
||||||
|
import { TagService } from './TagService';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightweight snapshot of metadata required for advanced search filters.
|
||||||
|
*/
|
||||||
|
interface CachedMetadata {
|
||||||
|
author?: string;
|
||||||
|
copyright?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a structured filter derived from the freeform query.
|
||||||
|
*/
|
||||||
|
interface AdvancedFilter {
|
||||||
|
field: 'author' | 'copyright';
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides fuzzy search capabilities backed by Fuse.js and the database content.
|
||||||
|
*/
|
||||||
|
export class SearchService {
|
||||||
|
private fuse: Fuse<AudioFileSummary> | null = null;
|
||||||
|
private cache: AudioFileSummary[] = [];
|
||||||
|
private readonly metadataCache = new Map<number, CachedMetadata>();
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private readonly database: DatabaseService,
|
||||||
|
private readonly tagService: TagService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rebuilds the in-memory index from the current database contents.
|
||||||
|
*/
|
||||||
|
public rebuildIndex(): void {
|
||||||
|
this.cache = this.database.listFiles();
|
||||||
|
this.metadataCache.clear();
|
||||||
|
this.fuse = new Fuse(this.cache, {
|
||||||
|
includeScore: true,
|
||||||
|
threshold: 0.35,
|
||||||
|
keys: [
|
||||||
|
{ name: 'displayName', weight: 0.3 },
|
||||||
|
{ name: 'fileName', weight: 0.2 },
|
||||||
|
{ name: 'relativePath', weight: 0.2 },
|
||||||
|
{ name: 'tags', weight: 0.2 },
|
||||||
|
{ name: 'categories', weight: 0.1 }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a fuzzy search query, falling back to the full collection when the input is empty.
|
||||||
|
*/
|
||||||
|
public search(query: string): AudioFileSummary[] {
|
||||||
|
const trimmed = query.trim();
|
||||||
|
if (trimmed.length === 0) {
|
||||||
|
return this.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { filters, remainingQuery } = this.extractAdvancedFilters(trimmed);
|
||||||
|
let candidates = this.getAll();
|
||||||
|
|
||||||
|
if (filters.length > 0) {
|
||||||
|
candidates = candidates.filter((file) => this.matchesAdvancedFilters(file, filters));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remainingQuery.length === 0) {
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fuseInstance = filters.length > 0
|
||||||
|
? new Fuse(candidates, this.createFuseOptions())
|
||||||
|
: this.ensureFuse();
|
||||||
|
|
||||||
|
return fuseInstance
|
||||||
|
.search(remainingQuery)
|
||||||
|
.map((result: Fuse.FuseResult<AudioFileSummary>) => result.item)
|
||||||
|
.filter((file) => (filters.length === 0 ? true : this.matchesAdvancedFilters(file, filters)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the cached collection, lazily rebuilding the index on first access.
|
||||||
|
*/
|
||||||
|
public getAll(): AudioFileSummary[] {
|
||||||
|
if (this.cache.length === 0) {
|
||||||
|
this.rebuildIndex();
|
||||||
|
}
|
||||||
|
return this.cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures a Fuse instance is available, recreating it when the cache is stale.
|
||||||
|
*/
|
||||||
|
private ensureFuse(): Fuse<AudioFileSummary> {
|
||||||
|
if (!this.fuse) {
|
||||||
|
this.rebuildIndex();
|
||||||
|
}
|
||||||
|
return this.fuse ?? new Fuse(this.getAll(), this.createFuseOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides the standard Fuse configuration used by the service.
|
||||||
|
*/
|
||||||
|
private createFuseOptions(): Fuse.IFuseOptions<AudioFileSummary> {
|
||||||
|
return {
|
||||||
|
includeScore: true,
|
||||||
|
threshold: 0.35,
|
||||||
|
keys: [
|
||||||
|
{ name: 'displayName', weight: 0.3 },
|
||||||
|
{ name: 'fileName', weight: 0.2 },
|
||||||
|
{ name: 'relativePath', weight: 0.2 },
|
||||||
|
{ name: 'tags', weight: 0.2 },
|
||||||
|
{ name: 'categories', weight: 0.1 }
|
||||||
|
]
|
||||||
|
} satisfies Fuse.IFuseOptions<AudioFileSummary>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pulls out author/copyright filters and returns the remaining free text query.
|
||||||
|
*/
|
||||||
|
private extractAdvancedFilters(query: string): { filters: AdvancedFilter[]; remainingQuery: string } {
|
||||||
|
const tokens = query.split(/\s+/);
|
||||||
|
const filters: AdvancedFilter[] = [];
|
||||||
|
const remainingTokens: string[] = [];
|
||||||
|
|
||||||
|
for (const token of tokens) {
|
||||||
|
const lower = token.toLowerCase();
|
||||||
|
if (lower.startsWith('author:')) {
|
||||||
|
const value = token.slice('author:'.length).trim();
|
||||||
|
if (value.length > 0) {
|
||||||
|
filters.push({ field: 'author', value: value.toLowerCase() });
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (lower.startsWith('copyright:')) {
|
||||||
|
const value = token.slice('copyright:'.length).trim();
|
||||||
|
if (value.length > 0) {
|
||||||
|
filters.push({ field: 'copyright', value: value.toLowerCase() });
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
remainingTokens.push(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
filters,
|
||||||
|
remainingQuery: remainingTokens.join(' ').trim()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates whether a file satisfies all extracted advanced filters.
|
||||||
|
*/
|
||||||
|
private matchesAdvancedFilters(file: AudioFileSummary, filters: AdvancedFilter[]): boolean {
|
||||||
|
if (filters.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = this.getCachedMetadata(file);
|
||||||
|
return filters.every((filter) => {
|
||||||
|
const source = filter.field === 'author' ? metadata.author : metadata.copyright;
|
||||||
|
if (!source) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return source.toLowerCase().includes(filter.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads metadata once per file and caches it for reuse across searches in the same session.
|
||||||
|
*/
|
||||||
|
private getCachedMetadata(file: AudioFileSummary): CachedMetadata {
|
||||||
|
const cached = this.metadataCache.get(file.id);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = this.tagService.readMetadata(file.absolutePath);
|
||||||
|
const normalized: CachedMetadata = {
|
||||||
|
author: metadata.author?.trim() || undefined,
|
||||||
|
copyright: metadata.copyright?.trim() || undefined
|
||||||
|
};
|
||||||
|
this.metadataCache.set(file.id, normalized);
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/main/services/SettingsService.ts
Normal file
60
src/main/services/SettingsService.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { app } from 'electron';
|
||||||
|
import { AppSettings } from '../../shared/models';
|
||||||
|
import { DatabaseService } from './DatabaseService';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages persistent application settings backed by the SQLite database.
|
||||||
|
*/
|
||||||
|
export class SettingsService {
|
||||||
|
public constructor(private readonly database: DatabaseService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the current settings snapshot.
|
||||||
|
*/
|
||||||
|
public getSettings(): AppSettings {
|
||||||
|
return this.database.getSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the configured library path or null when not set.
|
||||||
|
*/
|
||||||
|
public getLibraryPath(): string | null {
|
||||||
|
return this.database.getSettings().libraryPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures there is a library path stored, defaulting to the user's Music directory.
|
||||||
|
*/
|
||||||
|
public ensureLibraryPath(): string {
|
||||||
|
const settings = this.database.getSettings();
|
||||||
|
if (settings.libraryPath) {
|
||||||
|
this.ensureDirectory(settings.libraryPath);
|
||||||
|
return settings.libraryPath;
|
||||||
|
}
|
||||||
|
const defaultPath = path.join(app.getPath('music'), 'AudioSortLibrary');
|
||||||
|
this.ensureDirectory(defaultPath);
|
||||||
|
this.database.setSetting('libraryPath', defaultPath);
|
||||||
|
return defaultPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the stored library path after validating and normalising it.
|
||||||
|
*/
|
||||||
|
public updateLibraryPath(targetPath: string): AppSettings {
|
||||||
|
const normalised = path.resolve(targetPath);
|
||||||
|
this.ensureDirectory(normalised);
|
||||||
|
this.database.setSetting('libraryPath', normalised);
|
||||||
|
return this.database.getSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guarantees that the desired directory exists.
|
||||||
|
*/
|
||||||
|
private ensureDirectory(dirPath: string): void {
|
||||||
|
if (!fs.existsSync(dirPath)) {
|
||||||
|
fs.mkdirSync(dirPath, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
217
src/main/services/TagService.ts
Normal file
217
src/main/services/TagService.ts
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
import fs from 'node:fs';
|
||||||
|
import { WaveFile } from 'wavefile';
|
||||||
|
import { AudioFileSummary } from '../../shared/models';
|
||||||
|
import { DatabaseService } from './DatabaseService';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coordinates tag persistence between the database and embedded WAV metadata.
|
||||||
|
*/
|
||||||
|
export class TagService {
|
||||||
|
public constructor(private readonly database: DatabaseService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies tags and categories to a file record and embeds the metadata into the WAV container.
|
||||||
|
*/
|
||||||
|
public applyTagging(fileId: number, tags: string[], categories: string[]): AudioFileSummary {
|
||||||
|
const normalisedTags = this.normaliseValues(tags);
|
||||||
|
const normalisedCategories = this.normaliseValues(categories);
|
||||||
|
const updated = this.database.updateTagging(fileId, normalisedTags, normalisedCategories);
|
||||||
|
this.writeWaveMetadata(updated.absolutePath, {
|
||||||
|
tags: normalisedTags,
|
||||||
|
categories: normalisedCategories
|
||||||
|
});
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalises incoming arrays by trimming whitespace, removing empties, and deduplicating.
|
||||||
|
*/
|
||||||
|
private normaliseValues(values: string[]): string[] {
|
||||||
|
const result = new Set<string>();
|
||||||
|
for (const value of values) {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (trimmed.length > 0) {
|
||||||
|
result.add(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads embedded metadata from a WAV file.
|
||||||
|
*/
|
||||||
|
public readMetadata(filePath: string): {
|
||||||
|
author?: string;
|
||||||
|
copyright?: string;
|
||||||
|
title?: string;
|
||||||
|
rating?: number;
|
||||||
|
} {
|
||||||
|
try {
|
||||||
|
const buffer = fs.readFileSync(filePath);
|
||||||
|
const wave = new WaveFile(buffer);
|
||||||
|
const tags = this.extractInfoTagMap(wave);
|
||||||
|
const ratingValue = tags.IRTD ? Number.parseInt(tags.IRTD, 10) : undefined;
|
||||||
|
let rating: number | undefined;
|
||||||
|
if (typeof ratingValue === 'number' && Number.isFinite(ratingValue)) {
|
||||||
|
rating = Math.floor(ratingValue / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
author: tags.IART?.trim() || undefined,
|
||||||
|
copyright: tags.ICOP?.trim() || undefined,
|
||||||
|
title: tags.INAM?.trim() || undefined,
|
||||||
|
rating
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console -- Logging to devtools console is helpful for diagnosis.
|
||||||
|
console.warn('Failed to read WAV metadata', error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes tags and categories to an organized file as embedded metadata.
|
||||||
|
*/
|
||||||
|
public writeMetadataOnly(
|
||||||
|
filePath: string,
|
||||||
|
metadata: {
|
||||||
|
tags: string[];
|
||||||
|
categories: string[];
|
||||||
|
title?: string | null;
|
||||||
|
author?: string | null;
|
||||||
|
copyright?: string | null;
|
||||||
|
rating?: number;
|
||||||
|
}
|
||||||
|
): void {
|
||||||
|
this.writeWaveMetadata(filePath, metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a simple INFO chunk with the provided metadata. Failures are swallowed so DB state remains authoritative.
|
||||||
|
*/
|
||||||
|
private writeWaveMetadata(
|
||||||
|
filePath: string,
|
||||||
|
metadata: {
|
||||||
|
tags: string[];
|
||||||
|
categories: string[];
|
||||||
|
title?: string | null;
|
||||||
|
author?: string | null;
|
||||||
|
copyright?: string | null;
|
||||||
|
rating?: number;
|
||||||
|
}
|
||||||
|
): void {
|
||||||
|
try {
|
||||||
|
const buffer = fs.readFileSync(filePath);
|
||||||
|
const wave = new WaveFile(buffer);
|
||||||
|
const waveWithInfo = wave as WaveFile & {
|
||||||
|
setTag?: (tag: string, value: string) => void;
|
||||||
|
listInfoTags?: Record<string, string>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tagValuesList = metadata.tags.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
||||||
|
const categoryValuesList = metadata.categories.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
||||||
|
|
||||||
|
const tagText = tagValuesList.length > 0 ? tagValuesList.join('; ') : null;
|
||||||
|
const categoryText = categoryValuesList.length > 0 ? categoryValuesList.join('; ') : null;
|
||||||
|
const primaryCategory = categoryValuesList.at(0) ?? null;
|
||||||
|
|
||||||
|
const tagValues: Record<string, string | null> = {
|
||||||
|
IKEY: tagText,
|
||||||
|
ICMT: tagText,
|
||||||
|
ISBJ: categoryText,
|
||||||
|
ISUB: primaryCategory,
|
||||||
|
IGNR: null,
|
||||||
|
INAM: metadata.title?.trim()?.length ? metadata.title.trim() : null,
|
||||||
|
IART: metadata.author?.trim()?.length ? metadata.author.trim() : null,
|
||||||
|
ICOP: metadata.copyright?.trim()?.length ? metadata.copyright.trim() : null,
|
||||||
|
IRTD: metadata.rating && metadata.rating > 0 ? String(metadata.rating * 2) : null,
|
||||||
|
ISFT: 'AudioSort'
|
||||||
|
};
|
||||||
|
|
||||||
|
this.applyInfoTags(waveWithInfo, tagValues);
|
||||||
|
|
||||||
|
const updatedBuffer = wave.toBuffer();
|
||||||
|
fs.writeFileSync(filePath, updatedBuffer);
|
||||||
|
|
||||||
|
console.log(`Wrote metadata to ${filePath}:`, {
|
||||||
|
tags: metadata.tags.join(', '),
|
||||||
|
categories: metadata.categories.join(', '),
|
||||||
|
title: metadata.title,
|
||||||
|
author: metadata.author,
|
||||||
|
copyright: metadata.copyright,
|
||||||
|
rating: metadata.rating
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console -- Logging to devtools console is helpful for diagnosis.
|
||||||
|
console.warn('Failed to write WAV metadata', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the INFO chunk tag map for low-level metadata consumers.
|
||||||
|
*/
|
||||||
|
public readInfoTags(filePath: string): Partial<Record<string, string>> {
|
||||||
|
try {
|
||||||
|
const buffer = fs.readFileSync(filePath);
|
||||||
|
const wave = new WaveFile(buffer);
|
||||||
|
return this.extractInfoTagMap(wave);
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console -- Metadata parsing failures are non-fatal.
|
||||||
|
console.warn('Failed to read WAV info tags', error);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies INFO chunk updates, clearing values when the metadata source is empty.
|
||||||
|
*/
|
||||||
|
private applyInfoTags(
|
||||||
|
wave: WaveFile & { setTag?: (tag: string, value: string) => void; listInfoTags?: Record<string, string> },
|
||||||
|
entries: Record<string, string | null>
|
||||||
|
): void {
|
||||||
|
const listInfo = wave.listInfoTags ?? null;
|
||||||
|
for (const [key, value] of Object.entries(entries)) {
|
||||||
|
const normalised = value ?? '';
|
||||||
|
if (typeof wave.setTag === 'function') {
|
||||||
|
wave.setTag(key, normalised);
|
||||||
|
}
|
||||||
|
if (listInfo) {
|
||||||
|
if (normalised.length === 0) {
|
||||||
|
delete listInfo[key];
|
||||||
|
} else {
|
||||||
|
listInfo[key] = normalised;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts INFO chunk entries from a WaveFile instance.
|
||||||
|
*/
|
||||||
|
private extractInfoTagMap(wave: WaveFile): Partial<Record<string, string>> {
|
||||||
|
const waveWithTags = wave as WaveFile & {
|
||||||
|
listTags?: () => Record<string, string>;
|
||||||
|
LIST?: Array<{ format: string; subChunks: Array<{ chunkId: string; value: string }> }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (typeof waveWithTags.listTags === 'function') {
|
||||||
|
return waveWithTags.listTags() as Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(waveWithTags.LIST)) {
|
||||||
|
const listEntries = waveWithTags.LIST as Array<{
|
||||||
|
format: string;
|
||||||
|
subChunks: Array<{ chunkId: string; value: string }>;
|
||||||
|
}>;
|
||||||
|
const infoChunk = listEntries.find((entry) => entry.format === 'INFO');
|
||||||
|
if (infoChunk) {
|
||||||
|
return infoChunk.subChunks.reduce<Record<string, string>>((acc, chunk) => {
|
||||||
|
acc[chunk.chunkId] = chunk.value;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, string>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
83
src/preload/index.ts
Normal file
83
src/preload/index.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { contextBridge, ipcRenderer } from 'electron';
|
||||||
|
import { IPC_CHANNELS, type RendererApi } from '../shared/ipc';
|
||||||
|
import type {
|
||||||
|
AppSettings,
|
||||||
|
AudioBufferPayload,
|
||||||
|
AudioFileSummary,
|
||||||
|
CategoryRecord,
|
||||||
|
LibraryScanSummary,
|
||||||
|
TagUpdatePayload
|
||||||
|
} from '../shared/models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preload bridge exposing a curated API surface to the renderer process.
|
||||||
|
*/
|
||||||
|
const api: RendererApi = {
|
||||||
|
async getSettings(): Promise<AppSettings> {
|
||||||
|
return ipcRenderer.invoke(IPC_CHANNELS.settingsGet);
|
||||||
|
},
|
||||||
|
async selectLibraryDirectory(): Promise<string | null> {
|
||||||
|
return ipcRenderer.invoke(IPC_CHANNELS.dialogSelectLibrary);
|
||||||
|
},
|
||||||
|
async setLibraryPath(path: string): Promise<AppSettings> {
|
||||||
|
return ipcRenderer.invoke(IPC_CHANNELS.settingsSetLibrary, path);
|
||||||
|
},
|
||||||
|
async rescanLibrary(): Promise<LibraryScanSummary> {
|
||||||
|
return ipcRenderer.invoke(IPC_CHANNELS.libraryScan);
|
||||||
|
},
|
||||||
|
async listAudioFiles(): Promise<AudioFileSummary[]> {
|
||||||
|
return ipcRenderer.invoke(IPC_CHANNELS.libraryList);
|
||||||
|
},
|
||||||
|
async listDuplicates(): Promise<{ checksum: string; files: AudioFileSummary[] }[]> {
|
||||||
|
return ipcRenderer.invoke(IPC_CHANNELS.libraryDuplicates);
|
||||||
|
},
|
||||||
|
async renameFile(fileId: number, newFileName: string): Promise<AudioFileSummary> {
|
||||||
|
return ipcRenderer.invoke(IPC_CHANNELS.libraryRename, fileId, newFileName);
|
||||||
|
},
|
||||||
|
async moveFile(fileId: number, targetRelativeDirectory: string): Promise<AudioFileSummary> {
|
||||||
|
return ipcRenderer.invoke(IPC_CHANNELS.libraryMove, fileId, targetRelativeDirectory);
|
||||||
|
},
|
||||||
|
async organizeFile(fileId: number, metadata: { customName?: string | null; author?: string | null; copyright?: string | null; rating?: number }): Promise<AudioFileSummary> {
|
||||||
|
return ipcRenderer.invoke(IPC_CHANNELS.libraryOrganize, fileId, metadata);
|
||||||
|
},
|
||||||
|
async updateCustomName(fileId: number, customName: string | null): Promise<AudioFileSummary> {
|
||||||
|
return ipcRenderer.invoke(IPC_CHANNELS.libraryCustomName, fileId, customName);
|
||||||
|
},
|
||||||
|
async openFileFolder(fileId: number): Promise<void> {
|
||||||
|
return ipcRenderer.invoke(IPC_CHANNELS.libraryOpenFolder, fileId);
|
||||||
|
},
|
||||||
|
async deleteFiles(fileIds: number[]): Promise<void> {
|
||||||
|
return ipcRenderer.invoke(IPC_CHANNELS.libraryDelete, fileIds);
|
||||||
|
},
|
||||||
|
async getAudioBuffer(fileId: number): Promise<AudioBufferPayload> {
|
||||||
|
return ipcRenderer.invoke(IPC_CHANNELS.libraryBuffer, fileId);
|
||||||
|
},
|
||||||
|
async getWaveformPreview(fileId: number, pointCount = 160): Promise<{ samples: number[]; rms: number }> {
|
||||||
|
return ipcRenderer.invoke(IPC_CHANNELS.libraryWaveformPreview, fileId, pointCount);
|
||||||
|
},
|
||||||
|
async updateTagging(payload: TagUpdatePayload): Promise<AudioFileSummary> {
|
||||||
|
return ipcRenderer.invoke(IPC_CHANNELS.tagsUpdate, payload);
|
||||||
|
},
|
||||||
|
async listCategories(): Promise<CategoryRecord[]> {
|
||||||
|
return ipcRenderer.invoke(IPC_CHANNELS.categoriesList);
|
||||||
|
},
|
||||||
|
async search(query: string): Promise<AudioFileSummary[]> {
|
||||||
|
return ipcRenderer.invoke(IPC_CHANNELS.searchQuery, query);
|
||||||
|
},
|
||||||
|
async readFileMetadata(fileId: number): Promise<{ author?: string; copyright?: string; title?: string; rating?: number }> {
|
||||||
|
return ipcRenderer.invoke(IPC_CHANNELS.libraryMetadata, fileId);
|
||||||
|
},
|
||||||
|
async listMetadataSuggestions(): Promise<{ authors: string[]; copyrights: string[] }> {
|
||||||
|
return ipcRenderer.invoke(IPC_CHANNELS.libraryMetadataSuggestions);
|
||||||
|
},
|
||||||
|
async updateFileMetadata(fileId: number, metadata: { author?: string | null; copyright?: string | null; rating?: number }): Promise<void> {
|
||||||
|
return ipcRenderer.invoke(IPC_CHANNELS.libraryUpdateMetadata, fileId, metadata);
|
||||||
|
},
|
||||||
|
onMenuAction(channel: string, callback: () => void): () => void {
|
||||||
|
const listener = () => callback();
|
||||||
|
ipcRenderer.on(channel, listener);
|
||||||
|
return () => ipcRenderer.removeListener(channel, listener);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld('api', api);
|
||||||
12
src/renderer/index.html
Normal file
12
src/renderer/index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>AudioSort</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
293
src/renderer/src/App.tsx
Normal file
293
src/renderer/src/App.tsx
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import FileList from './components/FileList';
|
||||||
|
import FileDetailPanel from './components/FileDetailPanel';
|
||||||
|
import MultiFileEditor from './components/MultiFileEditor';
|
||||||
|
import CategorySidebar from './components/CategorySidebar';
|
||||||
|
import AudioPlayer from './components/AudioPlayer';
|
||||||
|
import SettingsDialog from './components/SettingsDialog';
|
||||||
|
import DuplicateComparisonDialog from './components/DuplicateComparisonDialog';
|
||||||
|
import { useLibrarySnapshot } from './hooks/useLibrarySnapshot';
|
||||||
|
import { loadPlayerFile, usePlayerSnapshot } from './hooks/usePlayerSnapshot';
|
||||||
|
import { libraryStore, type CategoryFilterValue } from './stores/LibraryStore';
|
||||||
|
import type { AudioFileSummary } from '../../shared/models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Root renderer component orchestrating layout and interactions.
|
||||||
|
*/
|
||||||
|
function App(): JSX.Element {
|
||||||
|
const library = useLibrarySnapshot();
|
||||||
|
const player = usePlayerSnapshot();
|
||||||
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||||
|
const [duplicateGroups, setDuplicateGroups] = useState<{ checksum: string; files: AudioFileSummary[] }[] | null>(null);
|
||||||
|
const [showStatusMessage, setShowStatusMessage] = useState(false);
|
||||||
|
const [statusFadingOut, setStatusFadingOut] = useState(false);
|
||||||
|
|
||||||
|
const statusMessage = useMemo(() => {
|
||||||
|
if (!library.lastScan) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const { added, updated, removed } = library.lastScan;
|
||||||
|
return `Scan complete: +${added} updated ${updated} removed ${removed}`;
|
||||||
|
}, [library.lastScan]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (statusMessage) {
|
||||||
|
setShowStatusMessage(true);
|
||||||
|
setStatusFadingOut(false);
|
||||||
|
const fadeTimer = setTimeout(() => {
|
||||||
|
setStatusFadingOut(true);
|
||||||
|
}, 4500);
|
||||||
|
const hideTimer = setTimeout(() => {
|
||||||
|
setShowStatusMessage(false);
|
||||||
|
setStatusFadingOut(false);
|
||||||
|
}, 5000);
|
||||||
|
return () => {
|
||||||
|
clearTimeout(fadeTimer);
|
||||||
|
clearTimeout(hideTimer);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [statusMessage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const cleanup1 = window.api.onMenuAction('open-settings', () => setSettingsOpen(true));
|
||||||
|
const cleanup2 = window.api.onMenuAction('rescan-library', handleRescan);
|
||||||
|
const cleanup3 = window.api.onMenuAction('find-duplicates', handleFindDuplicates);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cleanup1();
|
||||||
|
cleanup2();
|
||||||
|
cleanup3();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const selectedFile = useMemo(
|
||||||
|
() => library.files.find((file) => file.id === library.selectedFileId) ?? null,
|
||||||
|
[library.files, library.selectedFileId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedFiles = useMemo(
|
||||||
|
() => library.files.filter((file) => library.selectedFileIds.has(file.id)),
|
||||||
|
[library.files, library.selectedFileIds]
|
||||||
|
);
|
||||||
|
|
||||||
|
const isMultiSelect = selectedFiles.length > 1;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isMultiSelect) {
|
||||||
|
loadPlayerFile(selectedFile, false);
|
||||||
|
}
|
||||||
|
}, [selectedFile?.id, isMultiSelect]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleArrowNavigation = (event: KeyboardEvent) => {
|
||||||
|
if (event.code !== 'ArrowUp' && event.code !== 'ArrowDown') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.altKey || event.metaKey || event.ctrlKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = event.target as HTMLElement | null;
|
||||||
|
if (target) {
|
||||||
|
const tagName = target.tagName;
|
||||||
|
const isEditable = tagName === 'INPUT' || tagName === 'TEXTAREA' || tagName === 'SELECT' || target.isContentEditable;
|
||||||
|
if (isEditable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
libraryStore.selectAdjacent(event.code === 'ArrowUp' ? -1 : 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleArrowNavigation);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handleArrowNavigation);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSearch = (value: string) => {
|
||||||
|
void libraryStore.search(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRescan = async () => {
|
||||||
|
await libraryStore.rescan();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFindDuplicates = async () => {
|
||||||
|
const duplicates = await window.api.listDuplicates();
|
||||||
|
setDuplicateGroups(duplicates);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeepDuplicate = async (fileIdToKeep: number, fileIdsToDelete: number[]) => {
|
||||||
|
await window.api.deleteFiles(fileIdsToDelete);
|
||||||
|
await libraryStore.bootstrap();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseDuplicates = () => {
|
||||||
|
setDuplicateGroups(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectFile = (fileId: number, options?: { multi?: boolean; range?: boolean }) => {
|
||||||
|
libraryStore.selectFile(fileId, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects every visible file so keyboard shortcuts mirror multi-select gestures.
|
||||||
|
*/
|
||||||
|
const handleSelectAllFiles = () => {
|
||||||
|
libraryStore.selectAllVisibleFiles();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePlayFile = (file: AudioFileSummary) => {
|
||||||
|
loadPlayerFile(file, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCategorySelect = (filter: CategoryFilterValue) => {
|
||||||
|
libraryStore.setCategoryFilter(filter);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRename = async (newName: string) => {
|
||||||
|
if (!selectedFile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await libraryStore.renameFile(selectedFile.id, newName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMove = async (targetDirectory: string) => {
|
||||||
|
if (!selectedFile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await libraryStore.moveFile(selectedFile.id, targetDirectory);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOrganize = async (metadata: { customName?: string | null; author?: string | null; copyright?: string | null; rating?: number }) => {
|
||||||
|
if (!selectedFile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await libraryStore.organizeFile(selectedFile.id, metadata);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateCustomName = async (customName: string | null) => {
|
||||||
|
if (!selectedFile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await libraryStore.updateCustomName(selectedFile.id, customName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTagUpdate = async (data: { tags: string[]; categories: string[] }) => {
|
||||||
|
if (!selectedFile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await libraryStore.updateTagging({ fileId: selectedFile.id, ...data });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMultiFileTagUpdate = async (fileId: number, data: { tags: string[]; categories: string[] }) => {
|
||||||
|
await libraryStore.updateTagging({ fileId, ...data });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMultiFileCustomName = async (fileId: number, customName: string | null) => {
|
||||||
|
await libraryStore.updateCustomName(fileId, customName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMultiFileOrganize = async (fileId: number, metadata: { customName?: string | null; author?: string | null; copyright?: string | null; rating?: number }) => {
|
||||||
|
await libraryStore.organizeFile(fileId, metadata);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMultiFileUpdateMetadata = async (fileId: number, metadata: { author?: string | null; copyright?: string | null; rating?: number }) => {
|
||||||
|
await libraryStore.updateFileMetadata(fileId, metadata);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDropFilesToCategory = async (fileIds: number[], categoryId: string) => {
|
||||||
|
for (const fileId of fileIds) {
|
||||||
|
const file = library.files.find((f) => f.id === fileId);
|
||||||
|
if (!file) continue;
|
||||||
|
|
||||||
|
const existingCategories = file.categories;
|
||||||
|
if (existingCategories.includes(categoryId)) continue;
|
||||||
|
|
||||||
|
const newCategories = [...existingCategories, categoryId];
|
||||||
|
await libraryStore.updateTagging({
|
||||||
|
fileId,
|
||||||
|
tags: file.tags,
|
||||||
|
categories: newCategories
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLibraryPath = async (path: string) => {
|
||||||
|
await libraryStore.setLibraryPath(path);
|
||||||
|
setSettingsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-root">
|
||||||
|
{showStatusMessage && statusMessage && (
|
||||||
|
<div className={`status-banner ${statusFadingOut ? 'fade-out' : ''}`}>
|
||||||
|
{statusMessage}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<main className="app-body">
|
||||||
|
<CategorySidebar
|
||||||
|
categories={library.categories}
|
||||||
|
files={library.files}
|
||||||
|
activeFilter={library.categoryFilter}
|
||||||
|
onSelect={handleCategorySelect}
|
||||||
|
onDropFiles={handleDropFilesToCategory}
|
||||||
|
/>
|
||||||
|
<FileList
|
||||||
|
files={library.visibleFiles}
|
||||||
|
selectedId={library.selectedFileId}
|
||||||
|
selectedIds={library.selectedFileIds}
|
||||||
|
onSelect={handleSelectFile}
|
||||||
|
onPlay={handlePlayFile}
|
||||||
|
searchValue={library.searchQuery}
|
||||||
|
onSearchChange={handleSearch}
|
||||||
|
onSelectAll={handleSelectAllFiles}
|
||||||
|
/>
|
||||||
|
<div className="app-details">
|
||||||
|
{isMultiSelect ? (
|
||||||
|
<MultiFileEditor
|
||||||
|
files={selectedFiles}
|
||||||
|
categories={library.categories}
|
||||||
|
onUpdateTags={handleMultiFileTagUpdate}
|
||||||
|
onUpdateCustomName={handleMultiFileCustomName}
|
||||||
|
onOrganize={handleMultiFileOrganize}
|
||||||
|
onUpdateMetadata={handleMultiFileUpdateMetadata}
|
||||||
|
metadataSuggestionsVersion={library.metadataSuggestionsVersion}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<FileDetailPanel
|
||||||
|
file={selectedFile}
|
||||||
|
categories={library.categories}
|
||||||
|
onRename={handleRename}
|
||||||
|
onMove={handleMove}
|
||||||
|
onOrganize={handleOrganize}
|
||||||
|
onUpdateTags={handleTagUpdate}
|
||||||
|
onUpdateCustomName={handleUpdateCustomName}
|
||||||
|
metadataSuggestionsVersion={library.metadataSuggestionsVersion}
|
||||||
|
/>
|
||||||
|
<AudioPlayer snapshot={player} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<SettingsDialog
|
||||||
|
open={settingsOpen}
|
||||||
|
currentPath={library.settings.libraryPath}
|
||||||
|
onClose={() => setSettingsOpen(false)}
|
||||||
|
onSelectDirectory={handleLibraryPath}
|
||||||
|
/>
|
||||||
|
{duplicateGroups && (
|
||||||
|
<DuplicateComparisonDialog
|
||||||
|
duplicateGroups={duplicateGroups}
|
||||||
|
onKeepFile={handleKeepDuplicate}
|
||||||
|
onClose={handleCloseDuplicates}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
361
src/renderer/src/components/AudioPlayer.tsx
Normal file
361
src/renderer/src/components/AudioPlayer.tsx
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import type { PlayerSnapshot } from '../stores/PlayerStore';
|
||||||
|
import { Waveform } from './Waveform';
|
||||||
|
|
||||||
|
const MIN_VOLUME_DB = -48;
|
||||||
|
const DEFAULT_VOLUME_DB = -24;
|
||||||
|
const VOLUME_STORAGE_KEY = 'audio-player.volume-slider';
|
||||||
|
|
||||||
|
const DEFAULT_VOLUME_SLIDER = (() => {
|
||||||
|
const range = 0 - MIN_VOLUME_DB;
|
||||||
|
if (range === 0) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const slider = (DEFAULT_VOLUME_DB - MIN_VOLUME_DB) / range;
|
||||||
|
if (!Number.isFinite(slider)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return Math.min(1, Math.max(0, slider));
|
||||||
|
})();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reads the persisted volume slider position from local storage, falling back to the default slider value.
|
||||||
|
*/
|
||||||
|
function loadStoredVolumeSlider(): number {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return DEFAULT_VOLUME_SLIDER;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const raw = window.localStorage.getItem(VOLUME_STORAGE_KEY);
|
||||||
|
if (raw === null) {
|
||||||
|
return DEFAULT_VOLUME_SLIDER;
|
||||||
|
}
|
||||||
|
const parsed = Number.parseFloat(raw);
|
||||||
|
if (!Number.isFinite(parsed)) {
|
||||||
|
return DEFAULT_VOLUME_SLIDER;
|
||||||
|
}
|
||||||
|
if (parsed < 0 || parsed > 1) {
|
||||||
|
return DEFAULT_VOLUME_SLIDER;
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
} catch {
|
||||||
|
return DEFAULT_VOLUME_SLIDER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists the volume slider position to local storage, guarding against storage errors.
|
||||||
|
*/
|
||||||
|
function persistVolumeSlider(value: number): void {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
window.localStorage.setItem(VOLUME_STORAGE_KEY, value.toFixed(4));
|
||||||
|
} catch {
|
||||||
|
// Ignored: local storage may be unavailable (private mode, quotas, etc.).
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AudioPlayerProps {
|
||||||
|
snapshot: PlayerSnapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Seamless audio player that integrates smoothly with the interface.
|
||||||
|
*/
|
||||||
|
export function AudioPlayer({ snapshot }: AudioPlayerProps): JSX.Element {
|
||||||
|
const audioRef = useRef<HTMLAudioElement | null>(null);
|
||||||
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
|
const [currentTime, setCurrentTime] = useState(0);
|
||||||
|
const [duration, setDuration] = useState(0);
|
||||||
|
const [volumeSlider, setVolumeSlider] = useState<number>(() => loadStoredVolumeSlider());
|
||||||
|
const volumeSliderRef = useRef(volumeSlider);
|
||||||
|
const isSettingVolumeRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
volumeSliderRef.current = volumeSlider;
|
||||||
|
persistVolumeSlider(volumeSlider);
|
||||||
|
}, [volumeSlider]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const audio = audioRef.current;
|
||||||
|
if (!audio) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const targetVolume = sliderToVolume(volumeSlider);
|
||||||
|
if (Math.abs(audio.volume - targetVolume) < 0.0001) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isSettingVolumeRef.current = true;
|
||||||
|
audio.volume = targetVolume;
|
||||||
|
setTimeout(() => {
|
||||||
|
isSettingVolumeRef.current = false;
|
||||||
|
}, 0);
|
||||||
|
}, [volumeSlider]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const audio = audioRef.current;
|
||||||
|
if (!audio || snapshot.status !== 'ready' || !snapshot.audioUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rafId: number | null = null;
|
||||||
|
|
||||||
|
if (!audio.paused) {
|
||||||
|
audio.pause();
|
||||||
|
}
|
||||||
|
setIsPlaying(false);
|
||||||
|
setCurrentTime(0);
|
||||||
|
setDuration(0);
|
||||||
|
|
||||||
|
|
||||||
|
const updateTime = () => {
|
||||||
|
if (audio && !audio.paused) {
|
||||||
|
setCurrentTime(audio.currentTime);
|
||||||
|
rafId = requestAnimationFrame(updateTime);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePlay = () => {
|
||||||
|
setIsPlaying(true);
|
||||||
|
rafId = requestAnimationFrame(updateTime);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePause = () => {
|
||||||
|
setIsPlaying(false);
|
||||||
|
if (rafId !== null) {
|
||||||
|
cancelAnimationFrame(rafId);
|
||||||
|
rafId = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEnded = () => {
|
||||||
|
setIsPlaying(false);
|
||||||
|
if (rafId !== null) {
|
||||||
|
cancelAnimationFrame(rafId);
|
||||||
|
rafId = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDurationChange = () => setDuration(audio.duration);
|
||||||
|
const handleVolumeChange = () => {
|
||||||
|
if (isSettingVolumeRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextSlider = volumeToSlider(audio.volume);
|
||||||
|
setVolumeSlider(nextSlider);
|
||||||
|
volumeSliderRef.current = nextSlider;
|
||||||
|
};
|
||||||
|
const handleLoadedMetadata = () => {
|
||||||
|
isSettingVolumeRef.current = true;
|
||||||
|
audio.volume = sliderToVolume(volumeSliderRef.current);
|
||||||
|
setTimeout(() => {
|
||||||
|
isSettingVolumeRef.current = false;
|
||||||
|
}, 0);
|
||||||
|
setCurrentTime(0);
|
||||||
|
setDuration(audio.duration);
|
||||||
|
if (snapshot.autoPlay) {
|
||||||
|
void audio.play().catch(() => undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
isSettingVolumeRef.current = true;
|
||||||
|
audio.volume = sliderToVolume(volumeSliderRef.current);
|
||||||
|
setTimeout(() => {
|
||||||
|
isSettingVolumeRef.current = false;
|
||||||
|
}, 0);
|
||||||
|
audio.addEventListener('play', handlePlay);
|
||||||
|
audio.addEventListener('pause', handlePause);
|
||||||
|
audio.addEventListener('ended', handleEnded);
|
||||||
|
audio.addEventListener('durationchange', handleDurationChange);
|
||||||
|
audio.addEventListener('volumechange', handleVolumeChange);
|
||||||
|
audio.addEventListener('loadedmetadata', handleLoadedMetadata);
|
||||||
|
|
||||||
|
audio.src = snapshot.audioUrl;
|
||||||
|
audio.load();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (rafId !== null) {
|
||||||
|
cancelAnimationFrame(rafId);
|
||||||
|
}
|
||||||
|
audio.removeEventListener('play', handlePlay);
|
||||||
|
audio.removeEventListener('pause', handlePause);
|
||||||
|
audio.removeEventListener('ended', handleEnded);
|
||||||
|
audio.removeEventListener('durationchange', handleDurationChange);
|
||||||
|
audio.removeEventListener('volumechange', handleVolumeChange);
|
||||||
|
audio.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
||||||
|
};
|
||||||
|
}, [snapshot.loadCount, snapshot.autoPlay]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.code !== 'Space') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = event.target as HTMLElement | null;
|
||||||
|
if (target) {
|
||||||
|
const tagName = target.tagName;
|
||||||
|
const isEditableElement = tagName === 'INPUT' || tagName === 'TEXTAREA' || target.isContentEditable;
|
||||||
|
if (isEditableElement) {
|
||||||
|
const isRangeInput = target instanceof HTMLInputElement && target.type === 'range';
|
||||||
|
if (!isRangeInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
togglePlayPause();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [isPlaying]);
|
||||||
|
|
||||||
|
const togglePlayPause = () => {
|
||||||
|
const audio = audioRef.current;
|
||||||
|
if (!audio) return;
|
||||||
|
if (audio.paused || audio.ended) {
|
||||||
|
void audio.play();
|
||||||
|
} else {
|
||||||
|
audio.pause();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSeek = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const audio = audioRef.current;
|
||||||
|
if (!audio) return;
|
||||||
|
const time = parseFloat(e.target.value);
|
||||||
|
audio.currentTime = time;
|
||||||
|
setCurrentTime(time);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVolumeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const audio = audioRef.current;
|
||||||
|
if (!audio) return;
|
||||||
|
const sliderPosition = Math.max(0, Math.min(1, parseFloat(e.target.value)));
|
||||||
|
const amplitude = sliderToVolume(sliderPosition);
|
||||||
|
audio.volume = amplitude;
|
||||||
|
setVolumeSlider(sliderPosition);
|
||||||
|
volumeSliderRef.current = sliderPosition;
|
||||||
|
};
|
||||||
|
|
||||||
|
const volumeAmplitude = sliderToVolume(volumeSlider);
|
||||||
|
const volumeIcon = volumeAmplitude === 0 ? '🔇' : volumeAmplitude < 0.4 ? '🔉' : '🔊';
|
||||||
|
const volumeDecibels = sliderToDecibels(volumeSlider);
|
||||||
|
const volumeLabel = formatDecibels(volumeDecibels);
|
||||||
|
|
||||||
|
const formatTime = (seconds: number): string => {
|
||||||
|
if (!isFinite(seconds)) return '00:00.000';
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = Math.floor(seconds % 60);
|
||||||
|
const ms = Math.floor((seconds % 1) * 1000);
|
||||||
|
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}.${ms.toString().padStart(3, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (snapshot.status === 'idle') {
|
||||||
|
return <div className="player player--empty">Select a file to play</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.status === 'loading') {
|
||||||
|
return <div className="player player--loading">Loading audio…</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.status === 'error') {
|
||||||
|
return <div className="player player--error">{snapshot.error}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="player">
|
||||||
|
<audio ref={audioRef} preload="metadata">
|
||||||
|
<track kind="captions" />
|
||||||
|
</audio>
|
||||||
|
<div className="player-controls">
|
||||||
|
<button type="button" className="player-button" onClick={togglePlayPause} aria-label={isPlaying ? 'Pause' : 'Play'}>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className={isPlaying ? 'player-button-icon player-button-icon--pause' : 'player-button-icon player-button-icon--play'}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div className="player-progress">
|
||||||
|
<span className="player-time">{formatTime(currentTime)}</span>
|
||||||
|
<div className="player-slider-container">
|
||||||
|
<Waveform
|
||||||
|
audioUrl={snapshot.audioUrl}
|
||||||
|
currentTime={currentTime}
|
||||||
|
duration={duration}
|
||||||
|
className="player-waveform"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
className="player-slider"
|
||||||
|
min="0"
|
||||||
|
max={duration || 0}
|
||||||
|
step="0.001"
|
||||||
|
value={currentTime}
|
||||||
|
onChange={handleSeek}
|
||||||
|
aria-label="Seek"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<span className="player-time">{formatTime(duration)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="player-volume">
|
||||||
|
<span className="player-volume-icon">{volumeIcon}</span>
|
||||||
|
<span className="player-volume-db">{volumeLabel}</span>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
className="player-slider player-slider--volume"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
step="0.01"
|
||||||
|
value={volumeSlider}
|
||||||
|
onChange={handleVolumeChange}
|
||||||
|
aria-label="Volume"
|
||||||
|
aria-valuetext={volumeLabel}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sliderToDecibels(value: number): number {
|
||||||
|
if (value <= 0) {
|
||||||
|
return Number.NEGATIVE_INFINITY;
|
||||||
|
}
|
||||||
|
const clamped = Math.max(0, Math.min(1, value));
|
||||||
|
return MIN_VOLUME_DB + (0 - MIN_VOLUME_DB) * clamped;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sliderToVolume(value: number): number {
|
||||||
|
if (value <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const decibels = sliderToDecibels(value);
|
||||||
|
return Math.pow(10, decibels / 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
function volumeToSlider(volume: number): number {
|
||||||
|
if (volume <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const decibels = 20 * Math.log10(volume);
|
||||||
|
if (!Number.isFinite(decibels)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const slider = (decibels - MIN_VOLUME_DB) / (0 - MIN_VOLUME_DB);
|
||||||
|
return Math.max(0, Math.min(1, slider));
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDecibels(decibels: number): string {
|
||||||
|
if (!Number.isFinite(decibels) || decibels <= MIN_VOLUME_DB + 0.1) {
|
||||||
|
return '-∞ dB';
|
||||||
|
}
|
||||||
|
return `${decibels.toFixed(1)} dB`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AudioPlayer;
|
||||||
147
src/renderer/src/components/CategorySidebar.tsx
Normal file
147
src/renderer/src/components/CategorySidebar.tsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import type { AudioFileSummary, CategoryRecord } from '../../../shared/models';
|
||||||
|
import { CATEGORY_FILTER_UNTAGGED, type CategoryFilterValue } from '../stores/LibraryStore';
|
||||||
|
import {
|
||||||
|
buildCategorySwatch,
|
||||||
|
createCategoryStyleVars,
|
||||||
|
formatCategoryLabel
|
||||||
|
} from '../utils/categoryColors';
|
||||||
|
|
||||||
|
export interface CategorySidebarProps {
|
||||||
|
categories: CategoryRecord[];
|
||||||
|
files: AudioFileSummary[];
|
||||||
|
activeFilter: CategoryFilterValue;
|
||||||
|
onSelect(filter: CategoryFilterValue): void;
|
||||||
|
onDropFiles?(fileIds: number[], categoryId: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deriveCounts(files: AudioFileSummary[]): {
|
||||||
|
total: number;
|
||||||
|
untagged: number;
|
||||||
|
categoryCounts: Map<string, number>;
|
||||||
|
} {
|
||||||
|
const categoryCounts = new Map<string, number>();
|
||||||
|
let untagged = 0;
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.categories.length === 0) {
|
||||||
|
untagged += 1;
|
||||||
|
}
|
||||||
|
for (const categoryId of file.categories) {
|
||||||
|
categoryCounts.set(categoryId, (categoryCounts.get(categoryId) ?? 0) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
total: files.length,
|
||||||
|
untagged,
|
||||||
|
categoryCounts
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CategorySidebar({ categories, files, activeFilter, onSelect, onDropFiles }: CategorySidebarProps): JSX.Element {
|
||||||
|
const { total, untagged, categoryCounts } = deriveCounts(files);
|
||||||
|
const swatchMap = useMemo(() => {
|
||||||
|
const map = new Map<string, ReturnType<typeof buildCategorySwatch>>();
|
||||||
|
for (const record of categories) {
|
||||||
|
const existing = map.get(record.category) ?? buildCategorySwatch(record.category);
|
||||||
|
map.set(record.category, existing);
|
||||||
|
map.set(record.id, existing);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}, [categories]);
|
||||||
|
const grouped = new Map<string, CategoryRecord[]>();
|
||||||
|
for (const record of categories) {
|
||||||
|
grouped.set(record.category, [...(grouped.get(record.category) ?? []), record]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedGroups = Array.from(grouped.entries()).sort(([a], [b]) => a.localeCompare(b));
|
||||||
|
|
||||||
|
const handleSelect = (filter: CategoryFilterValue) => {
|
||||||
|
onSelect(filter);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDragOver = (event: React.DragEvent) => {
|
||||||
|
if (event.dataTransfer.types.includes('application/audiosort-file')) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.dataTransfer.dropEffect = 'copy';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrop = (event: React.DragEvent, categoryId: string) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const data = event.dataTransfer.getData('application/audiosort-file');
|
||||||
|
if (!data || !onDropFiles) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { selectedIds } = JSON.parse(data) as { fileId: number; selectedIds: number[] };
|
||||||
|
const fileIds = selectedIds.length > 0 ? selectedIds : [];
|
||||||
|
if (fileIds.length > 0) {
|
||||||
|
onDropFiles(fileIds, categoryId);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to parse drag data:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className="category-sidebar">
|
||||||
|
<div className="category-sidebar__section">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="category-sidebar__item"
|
||||||
|
data-active={activeFilter === null}
|
||||||
|
onClick={() => handleSelect(null)}
|
||||||
|
>
|
||||||
|
<span className="category-sidebar__label">All Files</span>
|
||||||
|
<span className="category-sidebar__count">{total}</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="category-sidebar__item"
|
||||||
|
data-active={activeFilter === CATEGORY_FILTER_UNTAGGED}
|
||||||
|
onClick={() => handleSelect(CATEGORY_FILTER_UNTAGGED)}
|
||||||
|
>
|
||||||
|
<span className="category-sidebar__label">TO TAG</span>
|
||||||
|
<span className="category-sidebar__count">{untagged}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{sortedGroups.map(([groupName, records]) => {
|
||||||
|
const sortedRecords = records
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => a.subCategory.localeCompare(b.subCategory, undefined, { sensitivity: 'base' }));
|
||||||
|
const hasVisible = sortedRecords.some((record) => (categoryCounts.get(record.id) ?? 0) > 0);
|
||||||
|
if (!hasVisible) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="category-sidebar__section" key={groupName}>
|
||||||
|
<div className="category-sidebar__group">{formatCategoryLabel(groupName)}</div>
|
||||||
|
{sortedRecords.map((record) => {
|
||||||
|
const count = categoryCounts.get(record.id) ?? 0;
|
||||||
|
if (count === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const styleVars = createCategoryStyleVars(swatchMap.get(record.id));
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
key={record.id}
|
||||||
|
className="category-sidebar__item category-sidebar__item--child"
|
||||||
|
data-active={activeFilter === record.id}
|
||||||
|
onClick={() => handleSelect(record.id)}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDrop={(event) => handleDrop(event, record.id)}
|
||||||
|
style={styleVars}
|
||||||
|
>
|
||||||
|
<span className="category-sidebar__label">{formatCategoryLabel(record.subCategory)}</span>
|
||||||
|
<span className="category-sidebar__count">{count}</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CategorySidebar;
|
||||||
284
src/renderer/src/components/DuplicateComparisonDialog.tsx
Normal file
284
src/renderer/src/components/DuplicateComparisonDialog.tsx
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import type { AudioFileSummary } from '../../../shared/models';
|
||||||
|
import Waveform from './Waveform';
|
||||||
|
|
||||||
|
type DifferenceFlags = {
|
||||||
|
fileName: boolean;
|
||||||
|
directory: boolean;
|
||||||
|
sampleRate: boolean;
|
||||||
|
bitDepth: boolean;
|
||||||
|
duration: boolean;
|
||||||
|
size: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DuplicateComparisonDialogProps {
|
||||||
|
duplicateGroups: { checksum: string; files: AudioFileSummary[] }[];
|
||||||
|
onKeepFile(fileIdToKeep: number, fileIdsToDelete: number[]): Promise<void>;
|
||||||
|
onClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dialog for comparing and managing duplicate files.
|
||||||
|
*/
|
||||||
|
export function DuplicateComparisonDialog({ duplicateGroups, onKeepFile, onClose }: DuplicateComparisonDialogProps): JSX.Element {
|
||||||
|
const [currentGroupIndex, setCurrentGroupIndex] = useState(0);
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [waveformUrls, setWaveformUrls] = useState<Record<number, string>>({});
|
||||||
|
const waveformPoolRef = useRef<string[]>([]);
|
||||||
|
|
||||||
|
if (duplicateGroups.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="modal-overlay" onClick={onClose}>
|
||||||
|
<div className="modal-content duplicate-dialog" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<h2>No Duplicates Found</h2>
|
||||||
|
<p>No duplicate files were found in your library.</p>
|
||||||
|
<div className="modal-actions">
|
||||||
|
<button type="button" onClick={onClose} className="primary-button">
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentGroup = duplicateGroups[currentGroupIndex];
|
||||||
|
const referenceFile = currentGroup.files[0] ?? null;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
waveformPoolRef.current.forEach((url) => URL.revokeObjectURL(url));
|
||||||
|
waveformPoolRef.current = [];
|
||||||
|
setWaveformUrls({});
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const loadWaveforms = async () => {
|
||||||
|
const next: Record<number, string> = {};
|
||||||
|
await Promise.all(
|
||||||
|
currentGroup.files.map(async (file) => {
|
||||||
|
try {
|
||||||
|
const payload = await window.api.getAudioBuffer(file.id);
|
||||||
|
const blob = new Blob([payload.buffer], { type: payload.mimeType });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
waveformPoolRef.current.push(url);
|
||||||
|
next[file.id] = url;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to prepare waveform for duplicate file', file.id, error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!cancelled) {
|
||||||
|
setWaveformUrls(next);
|
||||||
|
} else {
|
||||||
|
Object.values(next).forEach((url) => URL.revokeObjectURL(url));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void loadWaveforms();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [currentGroup]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
waveformPoolRef.current.forEach((url) => URL.revokeObjectURL(url));
|
||||||
|
waveformPoolRef.current = [];
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const differences = useMemo(() => {
|
||||||
|
if (!referenceFile) {
|
||||||
|
return {} as Record<number, DifferenceFlags>;
|
||||||
|
}
|
||||||
|
const referenceDirectory = getDirectory(referenceFile);
|
||||||
|
return currentGroup.files.reduce((acc, file) => {
|
||||||
|
const directory = getDirectory(file);
|
||||||
|
acc[file.id] = {
|
||||||
|
fileName: file.fileName !== referenceFile.fileName,
|
||||||
|
directory: directory !== referenceDirectory,
|
||||||
|
sampleRate: file.sampleRate !== referenceFile.sampleRate,
|
||||||
|
bitDepth: file.bitDepth !== referenceFile.bitDepth,
|
||||||
|
duration: file.durationMs !== referenceFile.durationMs,
|
||||||
|
size: file.size !== referenceFile.size
|
||||||
|
};
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<number, DifferenceFlags>);
|
||||||
|
}, [currentGroup.files, referenceFile]);
|
||||||
|
|
||||||
|
const handleKeep = async (file: AudioFileSummary) => {
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
const idsToDelete = currentGroup.files.filter((candidate) => candidate.id !== file.id).map((candidate) => candidate.id);
|
||||||
|
await onKeepFile(file.id, idsToDelete);
|
||||||
|
|
||||||
|
if (currentGroupIndex < duplicateGroups.length - 1) {
|
||||||
|
setCurrentGroupIndex((index) => index + 1);
|
||||||
|
} else {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert(`Failed to delete duplicates: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSkip = () => {
|
||||||
|
if (currentGroupIndex < duplicateGroups.length - 1) {
|
||||||
|
setCurrentGroupIndex(currentGroupIndex + 1);
|
||||||
|
} else {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePrevGroup = () => {
|
||||||
|
if (currentGroupIndex > 0) {
|
||||||
|
setCurrentGroupIndex(currentGroupIndex - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNextGroup = () => {
|
||||||
|
if (currentGroupIndex < duplicateGroups.length - 1) {
|
||||||
|
setCurrentGroupIndex(currentGroupIndex + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal-overlay" onClick={onClose}>
|
||||||
|
<div className="modal-content duplicate-dialog" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className="duplicate-header">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ghost-button duplicate-nav-button"
|
||||||
|
onClick={handlePrevGroup}
|
||||||
|
disabled={busy || currentGroupIndex === 0}
|
||||||
|
>
|
||||||
|
← Prev
|
||||||
|
</button>
|
||||||
|
<div className="duplicate-header-summary">
|
||||||
|
<h2>Duplicate Group {currentGroupIndex + 1} / {duplicateGroups.length}</h2>
|
||||||
|
<p>Checksum {currentGroup.checksum.slice(0, 10)}… · {currentGroup.files.length} files match</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ghost-button duplicate-nav-button"
|
||||||
|
onClick={handleNextGroup}
|
||||||
|
disabled={busy || currentGroupIndex === duplicateGroups.length - 1}
|
||||||
|
>
|
||||||
|
Next →
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p className="duplicate-tip">Scroll to review every duplicate. Differences from the first file are highlighted.</p>
|
||||||
|
<div className="duplicate-card-scroll">
|
||||||
|
<div className="duplicate-card-list">
|
||||||
|
{currentGroup.files.map((file) => {
|
||||||
|
const directory = getDirectory(file);
|
||||||
|
const diff = differences[file.id] ?? {};
|
||||||
|
const waveformUrl = waveformUrls[file.id] ?? null;
|
||||||
|
const durationSeconds = (file.durationMs ?? 0) / 1000;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={file.id} className="duplicate-column duplicate-column--scroll">
|
||||||
|
<div className="duplicate-card">
|
||||||
|
<header className="duplicate-card-header">
|
||||||
|
<div>
|
||||||
|
<h3>{file.displayName}</h3>
|
||||||
|
<p>{directory}</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div className="duplicate-waveform">
|
||||||
|
{waveformUrl ? (
|
||||||
|
<Waveform
|
||||||
|
audioUrl={waveformUrl}
|
||||||
|
currentTime={0}
|
||||||
|
duration={durationSeconds}
|
||||||
|
className="duplicate-waveform-canvas"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="duplicate-waveform-placeholder">Preparing waveform…</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<dl className="duplicate-details">
|
||||||
|
<div className={diff.fileName ? 'duplicate-detail-row duplicate-detail-row--diff' : 'duplicate-detail-row'}>
|
||||||
|
<dt>Filename</dt>
|
||||||
|
<dd>{file.fileName}</dd>
|
||||||
|
</div>
|
||||||
|
<div className={diff.directory ? 'duplicate-detail-row duplicate-detail-row--diff' : 'duplicate-detail-row'}>
|
||||||
|
<dt>Directory</dt>
|
||||||
|
<dd>{directory}</dd>
|
||||||
|
</div>
|
||||||
|
<div className={diff.sampleRate ? 'duplicate-detail-row duplicate-detail-row--diff' : 'duplicate-detail-row'}>
|
||||||
|
<dt>Sample Rate</dt>
|
||||||
|
<dd>{file.sampleRate ? `${file.sampleRate} Hz` : 'Unknown'}</dd>
|
||||||
|
</div>
|
||||||
|
<div className={diff.bitDepth ? 'duplicate-detail-row duplicate-detail-row--diff' : 'duplicate-detail-row'}>
|
||||||
|
<dt>Bit Depth</dt>
|
||||||
|
<dd>{file.bitDepth ?? 'Unknown'}</dd>
|
||||||
|
</div>
|
||||||
|
<div className={diff.duration ? 'duplicate-detail-row duplicate-detail-row--diff' : 'duplicate-detail-row'}>
|
||||||
|
<dt>Duration</dt>
|
||||||
|
<dd>{formatDuration(file.durationMs)}</dd>
|
||||||
|
</div>
|
||||||
|
<div className={diff.size ? 'duplicate-detail-row duplicate-detail-row--diff' : 'duplicate-detail-row'}>
|
||||||
|
<dt>Size</dt>
|
||||||
|
<dd>{formatBytes(file.size)}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleKeep(file)}
|
||||||
|
disabled={busy}
|
||||||
|
className="primary-button"
|
||||||
|
>
|
||||||
|
Keep This File
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-actions">
|
||||||
|
<button type="button" onClick={handleSkip} className="ghost-button" disabled={busy}>
|
||||||
|
Skip This Group
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={onClose} className="ghost-button" disabled={busy}>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDirectory(file: AudioFileSummary): string {
|
||||||
|
return file.relativePath.replace(file.fileName, '') || '(root)';
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBytes(bytes: number): string {
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
let value = bytes;
|
||||||
|
let unitIndex = 0;
|
||||||
|
while (value >= 1024 && unitIndex < units.length - 1) {
|
||||||
|
value /= 1024;
|
||||||
|
unitIndex += 1;
|
||||||
|
}
|
||||||
|
return `${value.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDuration(durationMs: number | null): string {
|
||||||
|
if (!durationMs) {
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
const totalSeconds = durationMs / 1000;
|
||||||
|
const minutes = Math.floor(totalSeconds / 60);
|
||||||
|
const seconds = Math.floor(totalSeconds % 60);
|
||||||
|
const milliseconds = Math.floor(durationMs % 1000);
|
||||||
|
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DuplicateComparisonDialog;
|
||||||
432
src/renderer/src/components/FileDetailPanel.tsx
Normal file
432
src/renderer/src/components/FileDetailPanel.tsx
Normal file
@@ -0,0 +1,432 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type FormEvent } from 'react';
|
||||||
|
import type { AudioFileSummary, CategoryRecord } from '../../../shared/models';
|
||||||
|
import { TagEditor } from './TagEditor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props for FileDetailPanel component.
|
||||||
|
*/
|
||||||
|
export interface FileDetailPanelProps {
|
||||||
|
file: AudioFileSummary | null;
|
||||||
|
categories: CategoryRecord[];
|
||||||
|
onRename(newName: string): Promise<void>;
|
||||||
|
onMove(targetRelativeDirectory: string): Promise<void>;
|
||||||
|
/** Organizes a file with optional metadata fields (customName, author, copyright, rating 1-5). */
|
||||||
|
onOrganize(metadata: { customName?: string | null; author?: string | null; copyright?: string | null; rating?: number }): Promise<void>;
|
||||||
|
onUpdateTags(data: { tags: string[]; categories: string[] }): Promise<void>;
|
||||||
|
onUpdateCustomName(customName: string | null): Promise<void>;
|
||||||
|
metadataSuggestionsVersion: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays metadata for the selected file with rename, move, and tagging controls.
|
||||||
|
*/
|
||||||
|
export function FileDetailPanel({ file, categories, onRename, onMove, onOrganize, onUpdateTags, onUpdateCustomName, metadataSuggestionsVersion }: FileDetailPanelProps): JSX.Element {
|
||||||
|
const [isEditingCustomName, setIsEditingCustomName] = useState(false);
|
||||||
|
const [moveDraft, setMoveDraft] = useState('');
|
||||||
|
const [customNameDraft, setCustomNameDraft] = useState('');
|
||||||
|
const [authorDraft, setAuthorDraft] = useState('');
|
||||||
|
const [ratingDraft, setRatingDraft] = useState(0);
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const [isTagSectionExpanded, setIsTagSectionExpanded] = useState(true);
|
||||||
|
const [suggestions, setSuggestions] = useState<{ authors: string[] }>({ authors: [] });
|
||||||
|
const initialMetadataRef = useRef<{ author: string; copyright: string; rating: number; customName: string }>({
|
||||||
|
author: '',
|
||||||
|
copyright: '',
|
||||||
|
rating: 0,
|
||||||
|
customName: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const appendSuggestion = useCallback((value: string) => {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (trimmed.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSuggestions((previous) => {
|
||||||
|
if (previous.authors.some((entry) => entry.localeCompare(trimmed, undefined, { sensitivity: 'accent' }) === 0)) {
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
const nextList = [...previous.authors, trimmed].sort((a, b) => a.localeCompare(b));
|
||||||
|
return {
|
||||||
|
authors: nextList
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const filteredAuthorSuggestions = useMemo(() => {
|
||||||
|
if (suggestions.authors.length === 0) {
|
||||||
|
return [] as string[];
|
||||||
|
}
|
||||||
|
const query = authorDraft.trim().toLowerCase();
|
||||||
|
const base = suggestions.authors.filter((entry) => entry.toLowerCase() !== query);
|
||||||
|
if (query.length === 0) {
|
||||||
|
return base.slice(0, 5);
|
||||||
|
}
|
||||||
|
return base.filter((entry) => entry.toLowerCase().includes(query)).slice(0, 5);
|
||||||
|
}, [authorDraft, suggestions.authors]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
setSuggestions({ authors: [] });
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const result = await window.api.listMetadataSuggestions();
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSuggestions({
|
||||||
|
authors: result.authors
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (!cancelled) {
|
||||||
|
console.error('Failed to load metadata suggestions:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [metadataSuggestionsVersion]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!file) {
|
||||||
|
setMoveDraft('');
|
||||||
|
setCustomNameDraft('');
|
||||||
|
setAuthorDraft('');
|
||||||
|
setRatingDraft(0);
|
||||||
|
setIsEditingCustomName(false);
|
||||||
|
initialMetadataRef.current = { author: '', copyright: '', rating: 0, customName: '' };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setMoveDraft(getParentDirectory(file.relativePath));
|
||||||
|
setCustomNameDraft(file.customName ?? '');
|
||||||
|
setIsEditingCustomName(false);
|
||||||
|
const baseCustomName = (file.customName ?? '').trim();
|
||||||
|
initialMetadataRef.current = { author: '', copyright: '', rating: 0, customName: baseCustomName };
|
||||||
|
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const metadata = await window.api.readFileMetadata(file.id);
|
||||||
|
let customNameValue = baseCustomName;
|
||||||
|
if (metadata.title && metadata.title.trim().length > 0) {
|
||||||
|
customNameValue = metadata.title.trim();
|
||||||
|
setCustomNameDraft(customNameValue);
|
||||||
|
}
|
||||||
|
const authorValue = metadata.author?.trim() ?? '';
|
||||||
|
const ratingValue = metadata.rating ?? 0;
|
||||||
|
setAuthorDraft(authorValue);
|
||||||
|
setRatingDraft(ratingValue);
|
||||||
|
initialMetadataRef.current = {
|
||||||
|
author: authorValue,
|
||||||
|
copyright: '',
|
||||||
|
rating: ratingValue,
|
||||||
|
customName: customNameValue
|
||||||
|
};
|
||||||
|
if (authorValue.length > 0) {
|
||||||
|
appendSuggestion(authorValue);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to read file metadata:', error);
|
||||||
|
setAuthorDraft('');
|
||||||
|
setRatingDraft(0);
|
||||||
|
initialMetadataRef.current = { author: '', copyright: '', rating: 0, customName: baseCustomName };
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [appendSuggestion, file?.id]);
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return <section className="file-detail empty">Select a file to view its details.</section>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStartCustomNameEdit = () => {
|
||||||
|
setIsEditingCustomName(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCustomNameKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
event.currentTarget.blur();
|
||||||
|
} else if (event.key === 'Escape') {
|
||||||
|
setIsEditingCustomName(false);
|
||||||
|
if (file) {
|
||||||
|
setCustomNameDraft(file.customName ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMove = async (event: FormEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
await onMove(moveDraft.trim());
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCustomNameBlur = async () => {
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
setIsEditingCustomName(false);
|
||||||
|
|
||||||
|
const normalizedValue = customNameDraft.trim();
|
||||||
|
const previousValue = file.customName?.trim() ?? '';
|
||||||
|
if (normalizedValue === previousValue) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = normalizedValue.length > 0 ? normalizedValue : null;
|
||||||
|
let updateSucceeded = false;
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
await onUpdateCustomName(payload);
|
||||||
|
updateSucceeded = true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update custom name:', error);
|
||||||
|
setCustomNameDraft(file.customName ?? '');
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateSucceeded) {
|
||||||
|
await triggerOrganize({ customName: normalizedValue });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const triggerOrganize = async (overrides?: { author?: string | null; rating?: number; customName?: string | null }) => {
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextAuthorRaw = overrides?.author !== undefined ? overrides.author : authorDraft;
|
||||||
|
const nextRatingValue = overrides?.rating !== undefined ? overrides.rating : ratingDraft;
|
||||||
|
const nextCustomNameRaw =
|
||||||
|
overrides?.customName !== undefined ? overrides.customName ?? '' : customNameDraft;
|
||||||
|
|
||||||
|
const trimmedAuthor = (nextAuthorRaw ?? '').trim();
|
||||||
|
const trimmedCustomName = (nextCustomNameRaw ?? '').toString().trim();
|
||||||
|
const comparisonState = {
|
||||||
|
author: trimmedAuthor,
|
||||||
|
copyright: '',
|
||||||
|
rating: nextRatingValue,
|
||||||
|
customName: trimmedCustomName
|
||||||
|
};
|
||||||
|
|
||||||
|
const previous = initialMetadataRef.current;
|
||||||
|
if (
|
||||||
|
previous &&
|
||||||
|
previous.author === comparisonState.author &&
|
||||||
|
previous.copyright === comparisonState.copyright &&
|
||||||
|
previous.rating === comparisonState.rating &&
|
||||||
|
previous.customName === comparisonState.customName
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
const authorPayload = trimmedAuthor.length > 0 ? trimmedAuthor : null;
|
||||||
|
if (file.categories.length === 0) {
|
||||||
|
// No categories - just save metadata without organizing
|
||||||
|
await window.api.updateFileMetadata(file.id, {
|
||||||
|
author: authorPayload,
|
||||||
|
rating: comparisonState.rating > 0 ? comparisonState.rating : undefined
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Has categories - organize the file
|
||||||
|
await onOrganize({
|
||||||
|
customName: trimmedCustomName.length > 0 ? trimmedCustomName : null,
|
||||||
|
author: authorPayload,
|
||||||
|
rating: comparisonState.rating > 0 ? comparisonState.rating : undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
initialMetadataRef.current = comparisonState;
|
||||||
|
if (trimmedAuthor.length > 0) {
|
||||||
|
appendSuggestion(trimmedAuthor);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save metadata:', error);
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTagSave = async (data: { tags: string[]; categories: string[] }) => {
|
||||||
|
setBusy(true);
|
||||||
|
try {
|
||||||
|
await onUpdateTags(data);
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenFolder = async () => {
|
||||||
|
if (!file) return;
|
||||||
|
try {
|
||||||
|
await window.api.openFileFolder(file.id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to open folder:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleTagSection = () => {
|
||||||
|
setIsTagSectionExpanded((value) => !value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="file-detail">
|
||||||
|
<header className="file-detail-header">
|
||||||
|
<div>
|
||||||
|
{isEditingCustomName ? (
|
||||||
|
<input
|
||||||
|
className="file-name-input"
|
||||||
|
value={customNameDraft}
|
||||||
|
onChange={(event: ChangeEvent<HTMLInputElement>) => setCustomNameDraft(event.target.value)}
|
||||||
|
onBlur={handleCustomNameBlur}
|
||||||
|
onKeyDown={handleCustomNameKeyDown}
|
||||||
|
disabled={busy}
|
||||||
|
placeholder="Enter custom name"
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<h1 className="file-name-editable" onClick={handleStartCustomNameEdit} title="Click to edit name">
|
||||||
|
{file.customName || file.displayName}
|
||||||
|
</h1>
|
||||||
|
)}
|
||||||
|
<p
|
||||||
|
onClick={handleOpenFolder}
|
||||||
|
style={{ cursor: 'pointer', opacity: 0.7, transition: 'opacity 0.2s ease' }}
|
||||||
|
onMouseEnter={(e) => e.currentTarget.style.opacity = '1'}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.style.opacity = '0.7'}
|
||||||
|
title="Click to open in folder"
|
||||||
|
>
|
||||||
|
{file.relativePath}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="file-meta-grid">
|
||||||
|
<div>{formatBytes(file.size)}</div>
|
||||||
|
<div>{formatDuration(file.durationMs)}</div>
|
||||||
|
<div>{file.sampleRate ? `${(file.sampleRate / 1000).toFixed(1)}kHz` : 'Unknown'}</div>
|
||||||
|
<div>{file.bitDepth ? `${file.bitDepth}-bit` : 'Unknown'}</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<form className="file-actions" onSubmit={(event) => event.preventDefault()}>
|
||||||
|
<div className="file-actions-grid">
|
||||||
|
<label>
|
||||||
|
<span>Author</span>
|
||||||
|
<input
|
||||||
|
value={authorDraft}
|
||||||
|
onChange={(event: ChangeEvent<HTMLInputElement>) => setAuthorDraft(event.target.value)}
|
||||||
|
onBlur={(event) => {
|
||||||
|
void triggerOrganize({ author: event.currentTarget.value });
|
||||||
|
}}
|
||||||
|
placeholder="Artist or creator name"
|
||||||
|
disabled={busy}
|
||||||
|
/>
|
||||||
|
{filteredAuthorSuggestions.length > 0 && (
|
||||||
|
<div className="metadata-suggestion-list" role="listbox" aria-label="Author suggestions">
|
||||||
|
{filteredAuthorSuggestions.map((option) => (
|
||||||
|
<button
|
||||||
|
key={option}
|
||||||
|
type="button"
|
||||||
|
className="metadata-suggestion"
|
||||||
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
|
onClick={() => {
|
||||||
|
setAuthorDraft(option);
|
||||||
|
void triggerOrganize({ author: option });
|
||||||
|
}}
|
||||||
|
disabled={busy}
|
||||||
|
>
|
||||||
|
{option}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Rating</span>
|
||||||
|
<div className="star-rating">
|
||||||
|
{[1, 2, 3, 4, 5].map((star) => (
|
||||||
|
<button
|
||||||
|
key={star}
|
||||||
|
type="button"
|
||||||
|
className={ratingDraft >= star ? 'star star--filled' : 'star'}
|
||||||
|
onClick={() => {
|
||||||
|
const nextValue = ratingDraft === star ? 0 : star;
|
||||||
|
setRatingDraft(nextValue);
|
||||||
|
void triggerOrganize({ rating: nextValue });
|
||||||
|
}}
|
||||||
|
disabled={busy}
|
||||||
|
aria-label={`${star} star${star > 1 ? 's' : ''}`}
|
||||||
|
>
|
||||||
|
★
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<section className="tag-editor-pane">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="tag-editor-toggle"
|
||||||
|
onClick={toggleTagSection}
|
||||||
|
aria-expanded={isTagSectionExpanded}
|
||||||
|
>
|
||||||
|
<span className="tag-editor-toggle-label">Tags</span>
|
||||||
|
<span className="tag-editor-toggle-icon" aria-hidden="true">
|
||||||
|
{isTagSectionExpanded ? 'v' : '>'}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<div className={isTagSectionExpanded ? 'tag-editor-content tag-editor-content--open' : 'tag-editor-content'} aria-hidden={!isTagSectionExpanded}>
|
||||||
|
<TagEditor
|
||||||
|
tags={file.tags}
|
||||||
|
categories={file.categories}
|
||||||
|
availableCategories={categories}
|
||||||
|
onSave={handleTagSave}
|
||||||
|
showHeading={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBytes(bytes: number): string {
|
||||||
|
const units = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
let value = bytes;
|
||||||
|
let unitIndex = 0;
|
||||||
|
while (value >= 1024 && unitIndex < units.length - 1) {
|
||||||
|
value /= 1024;
|
||||||
|
unitIndex += 1;
|
||||||
|
}
|
||||||
|
return `${value.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDuration(durationMs: number | null): string {
|
||||||
|
if (!durationMs) {
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
const totalSeconds = durationMs / 1000;
|
||||||
|
const minutes = Math.floor(totalSeconds / 60);
|
||||||
|
const seconds = Math.floor(totalSeconds % 60);
|
||||||
|
const milliseconds = Math.floor(durationMs % 1000);
|
||||||
|
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getParentDirectory(relativePath: string): string {
|
||||||
|
if (!relativePath || !relativePath.includes(pathSeparator())) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const segments = relativePath.split(pathSeparator());
|
||||||
|
segments.pop();
|
||||||
|
return segments.join(pathSeparator());
|
||||||
|
}
|
||||||
|
|
||||||
|
function pathSeparator(): string {
|
||||||
|
return typeof window !== 'undefined' && navigator.platform.startsWith('Win') ? '\\' : '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileDetailPanel;
|
||||||
582
src/renderer/src/components/FileList.tsx
Normal file
582
src/renderer/src/components/FileList.tsx
Normal file
@@ -0,0 +1,582 @@
|
|||||||
|
import { useEffect, useReducer, useRef } from 'react';
|
||||||
|
import type { AudioFileSummary } from '../../../shared/models';
|
||||||
|
|
||||||
|
export interface FileListProps {
|
||||||
|
files: AudioFileSummary[];
|
||||||
|
selectedId: number | null;
|
||||||
|
selectedIds: Set<number>;
|
||||||
|
onSelect(fileId: number, options: { multi?: boolean; range?: boolean }): void;
|
||||||
|
onPlay?(file: AudioFileSummary): void;
|
||||||
|
searchValue: string;
|
||||||
|
onSearchChange(value: string): void;
|
||||||
|
/**
|
||||||
|
* Invoked when the user requests to select every visible file (Ctrl+A).
|
||||||
|
*/
|
||||||
|
onSelectAll?(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vertical list of WAV files with highlighting for the active selection.
|
||||||
|
*/
|
||||||
|
export function FileList({ files, selectedId, selectedIds, onSelect, onPlay, searchValue, onSearchChange, onSelectAll }: FileListProps): JSX.Element {
|
||||||
|
const buttonRefs = useRef(new Map<number, HTMLButtonElement>());
|
||||||
|
const waveformCacheRef = useRef<Record<number, WaveformVisual>>({});
|
||||||
|
const [, forceWaveformUpdate] = useReducer((value: number) => value + 1, 0);
|
||||||
|
const dragGhostRef = useRef<HTMLElement | null>(null);
|
||||||
|
const dragRotation = useRef<number>(0);
|
||||||
|
const lastDragX = useRef<number>(0);
|
||||||
|
const dropTargetPosition = useRef<{ x: number; y: number } | null>(null);
|
||||||
|
const dragOffset = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
||||||
|
const dragStartTime = useRef<number>(0);
|
||||||
|
const listContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const button = buttonRefs.current.get(selectedId);
|
||||||
|
if (!button || document.activeElement === button) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const activeElement = document.activeElement as HTMLElement | null;
|
||||||
|
const isTypingContext = activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement;
|
||||||
|
const isEditable = activeElement?.getAttribute?.('contenteditable') === 'true';
|
||||||
|
if (isTypingContext || isEditable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Keep keyboard focus aligned with the active selection.
|
||||||
|
button.focus();
|
||||||
|
}, [selectedId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!window.api?.getWaveformPreview) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleIds = new Set(files.map((file) => file.id));
|
||||||
|
for (const cachedId of Object.keys(waveformCacheRef.current)) {
|
||||||
|
const numericId = Number.parseInt(cachedId, 10);
|
||||||
|
if (!visibleIds.has(numericId)) {
|
||||||
|
delete waveformCacheRef.current[numericId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
const loadPreviews = async () => {
|
||||||
|
for (const file of files) {
|
||||||
|
if (waveformCacheRef.current[file.id]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const preview = await window.api.getWaveformPreview(file.id, WAVEFORM_POINT_COUNT);
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const seed = file.checksum ?? file.absolutePath;
|
||||||
|
waveformCacheRef.current[file.id] = buildWaveformVisual(seed, preview.samples, preview.rms);
|
||||||
|
forceWaveformUpdate();
|
||||||
|
} catch (error) {
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.error(`Failed to load waveform preview for file ${file.id} (${file.fileName}):`, error);
|
||||||
|
const seed = file.checksum ?? file.absolutePath;
|
||||||
|
waveformCacheRef.current[file.id] = buildWaveformVisual(seed, null, null);
|
||||||
|
forceWaveformUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void loadPreviews();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [files]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!onSelectAll) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (!(event.ctrlKey || event.metaKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.key !== 'a' && event.key !== 'A') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeElement = document.activeElement as HTMLElement | null;
|
||||||
|
if (!activeElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const isTypingContext = activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement;
|
||||||
|
const isEditable = activeElement.getAttribute('contenteditable') === 'true';
|
||||||
|
if (isTypingContext || isEditable) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (listContainerRef.current && !listContainerRef.current.contains(activeElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
onSelectAll();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [onSelectAll]);
|
||||||
|
|
||||||
|
const handleClick = (fileId: number, event: React.MouseEvent) => {
|
||||||
|
onSelect(fileId, {
|
||||||
|
multi: event.ctrlKey || event.metaKey,
|
||||||
|
range: event.shiftKey
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDoubleClick = (file: AudioFileSummary) => {
|
||||||
|
if (onPlay) {
|
||||||
|
onPlay(file);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stripExtension = (filename: string): string => {
|
||||||
|
const lastDot = filename.lastIndexOf('.');
|
||||||
|
return lastDot > 0 ? filename.slice(0, lastDot) : filename;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTooltip = (file: AudioFileSummary): string => {
|
||||||
|
const customLabel = file.customName?.trim();
|
||||||
|
const title = customLabel && customLabel.length > 0 ? customLabel : file.displayName;
|
||||||
|
const sampleRate = file.sampleRate ? `${(file.sampleRate / 1000).toFixed(1)}kHz` : 'Unknown';
|
||||||
|
const bitDepth = file.bitDepth ? `${file.bitDepth}-bit` : 'Unknown';
|
||||||
|
const size = formatBytes(file.size);
|
||||||
|
return `${title}\n${sampleRate} • ${bitDepth} • ${size}\n${file.relativePath}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="file-list">
|
||||||
|
<div className="file-list-search">
|
||||||
|
<input
|
||||||
|
className="search-input"
|
||||||
|
value={searchValue}
|
||||||
|
onChange={(event) => onSearchChange(event.target.value)}
|
||||||
|
placeholder="Search files..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="file-list-items" ref={listContainerRef}>
|
||||||
|
{files.map((file) => {
|
||||||
|
const isActive = file.id === selectedId;
|
||||||
|
const isSelected = selectedIds.has(file.id);
|
||||||
|
const className = isActive
|
||||||
|
? 'file-item file-item--active'
|
||||||
|
: isSelected
|
||||||
|
? 'file-item file-item--selected'
|
||||||
|
: 'file-item';
|
||||||
|
const customLabel = file.customName?.trim();
|
||||||
|
const primaryLabel = customLabel && customLabel.length > 0 ? customLabel : file.displayName;
|
||||||
|
const seed = file.checksum ?? file.absolutePath;
|
||||||
|
const gradientId = `waveGradient-${file.id}`;
|
||||||
|
const wave = waveformCacheRef.current[file.id] ?? buildWaveformVisual(seed, null, null);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={file.id}
|
||||||
|
type="button"
|
||||||
|
className={className}
|
||||||
|
draggable={true}
|
||||||
|
ref={(node) => {
|
||||||
|
if (node) {
|
||||||
|
buttonRefs.current.set(file.id, node);
|
||||||
|
} else {
|
||||||
|
buttonRefs.current.delete(file.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onClick={(event) => handleClick(file.id, event)}
|
||||||
|
onDoubleClick={() => handleDoubleClick(file)}
|
||||||
|
onDragStart={(event) => {
|
||||||
|
// If dragging an unselected item, only drag that item
|
||||||
|
// If dragging a selected item, drag all selected items
|
||||||
|
const draggedIds = selectedIds.has(file.id) ? Array.from(selectedIds) : [file.id];
|
||||||
|
|
||||||
|
event.dataTransfer.setData('application/audiosort-file', JSON.stringify({
|
||||||
|
fileId: file.id,
|
||||||
|
selectedIds: draggedIds
|
||||||
|
}));
|
||||||
|
event.dataTransfer.effectAllowed = 'copy';
|
||||||
|
|
||||||
|
// Create transparent drag image to hide the default
|
||||||
|
const transparent = document.createElement('div');
|
||||||
|
transparent.style.width = '1px';
|
||||||
|
transparent.style.height = '1px';
|
||||||
|
transparent.style.opacity = '0';
|
||||||
|
document.body.appendChild(transparent);
|
||||||
|
event.dataTransfer.setDragImage(transparent, 0, 0);
|
||||||
|
setTimeout(() => transparent.remove(), 0);
|
||||||
|
|
||||||
|
// Calculate offset from click position to element center
|
||||||
|
const sourceElement = event.currentTarget as HTMLElement;
|
||||||
|
const rect = sourceElement.getBoundingClientRect();
|
||||||
|
const elementCenterX = rect.left + rect.width / 2;
|
||||||
|
const elementCenterY = rect.top + rect.height / 2;
|
||||||
|
dragOffset.current = {
|
||||||
|
x: elementCenterX - event.clientX,
|
||||||
|
y: elementCenterY - event.clientY
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate transition duration based on offset distance
|
||||||
|
const offsetDistance = Math.sqrt(
|
||||||
|
dragOffset.current.x * dragOffset.current.x +
|
||||||
|
dragOffset.current.y * dragOffset.current.y
|
||||||
|
);
|
||||||
|
const maxDistance = Math.sqrt(rect.width * rect.width + rect.height * rect.height) / 2;
|
||||||
|
const normalizedDistance = Math.min(offsetDistance / maxDistance, 1);
|
||||||
|
const transitionDuration = 0.1 + normalizedDistance * 0.2; // 0.1s to 0.3s
|
||||||
|
|
||||||
|
dragStartTime.current = Date.now();
|
||||||
|
|
||||||
|
// Clone the current element for custom floating preview
|
||||||
|
const ghost = sourceElement.cloneNode(true) as HTMLElement;
|
||||||
|
|
||||||
|
// Create a wrapper for rotation that doesn't affect position transition
|
||||||
|
const rotationWrapper = document.createElement('div');
|
||||||
|
rotationWrapper.style.position = 'fixed';
|
||||||
|
rotationWrapper.style.top = `${event.clientY}px`;
|
||||||
|
rotationWrapper.style.left = `${event.clientX}px`;
|
||||||
|
rotationWrapper.style.width = '0';
|
||||||
|
rotationWrapper.style.height = '0';
|
||||||
|
rotationWrapper.style.pointerEvents = 'none';
|
||||||
|
rotationWrapper.style.zIndex = '10000';
|
||||||
|
rotationWrapper.style.willChange = 'transform';
|
||||||
|
|
||||||
|
ghost.style.position = 'absolute';
|
||||||
|
ghost.style.top = '0';
|
||||||
|
ghost.style.left = '0';
|
||||||
|
ghost.style.width = `${sourceElement.offsetWidth}px`;
|
||||||
|
ghost.style.height = `${sourceElement.offsetHeight}px`;
|
||||||
|
ghost.style.opacity = '0.9';
|
||||||
|
ghost.style.contain = 'layout style paint';
|
||||||
|
ghost.style.transform = `translate(${-rect.width / 2 + dragOffset.current.x}px, ${-rect.height / 2 + dragOffset.current.y}px)`;
|
||||||
|
ghost.style.transition = `transform ${transitionDuration}s cubic-bezier(0.25, 0.1, 0.25, 1)`;
|
||||||
|
|
||||||
|
rotationWrapper.appendChild(ghost);
|
||||||
|
document.body.appendChild(rotationWrapper);
|
||||||
|
|
||||||
|
// Trigger the offset transition to center
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (ghost.parentNode) {
|
||||||
|
ghost.style.transform = `translate(-50%, -50%)`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
dragGhostRef.current = rotationWrapper;
|
||||||
|
lastDragX.current = event.clientX;
|
||||||
|
dragRotation.current = 0;
|
||||||
|
}}
|
||||||
|
onDrag={(event) => {
|
||||||
|
if (!dragGhostRef.current || (event.clientX === 0 && event.clientY === 0)) return;
|
||||||
|
|
||||||
|
const deltaX = event.clientX - lastDragX.current;
|
||||||
|
lastDragX.current = event.clientX;
|
||||||
|
|
||||||
|
const impulse = deltaX * 0.9;
|
||||||
|
const blended = dragRotation.current * 0.75 + impulse;
|
||||||
|
const clamped = Math.max(-12, Math.min(12, blended));
|
||||||
|
dragRotation.current = clamped;
|
||||||
|
|
||||||
|
// Update wrapper position (no rotation here)
|
||||||
|
dragGhostRef.current.style.top = `${event.clientY}px`;
|
||||||
|
dragGhostRef.current.style.left = `${event.clientX}px`;
|
||||||
|
|
||||||
|
// Apply rotation directly to wrapper without transition
|
||||||
|
dragGhostRef.current.style.transform = `rotate(${clamped}deg)`;
|
||||||
|
|
||||||
|
// Track position for potential drop animation
|
||||||
|
dropTargetPosition.current = { x: event.clientX, y: event.clientY };
|
||||||
|
}}
|
||||||
|
onDragEnd={(event) => {
|
||||||
|
const wrapper = dragGhostRef.current;
|
||||||
|
if (!wrapper) return;
|
||||||
|
|
||||||
|
// Check if this was a successful drop (dropEffect will be 'copy' if accepted)
|
||||||
|
const wasDropped = event.dataTransfer.dropEffect === 'copy';
|
||||||
|
|
||||||
|
if (wasDropped && dropTargetPosition.current) {
|
||||||
|
// Animate shrink into drop position
|
||||||
|
wrapper.style.transition = 'all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1)';
|
||||||
|
wrapper.style.transform = `rotate(0deg) scale(0)`;
|
||||||
|
wrapper.style.opacity = '0';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
wrapper.remove();
|
||||||
|
dragGhostRef.current = null;
|
||||||
|
}, 300);
|
||||||
|
} else {
|
||||||
|
// No drop or cancelled - remove immediately
|
||||||
|
wrapper.remove();
|
||||||
|
dragGhostRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
dragRotation.current = 0;
|
||||||
|
dropTargetPosition.current = null;
|
||||||
|
}}
|
||||||
|
title={formatTooltip(file)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="file-waveform"
|
||||||
|
viewBox={`0 0 ${WAVEFORM_WIDTH} ${WAVEFORM_HEIGHT}`}
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" stopColor={wave.gradientStart} />
|
||||||
|
<stop offset="100%" stopColor={wave.gradientEnd} />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<path d={wave.path} fill={`url(#${gradientId})`} stroke={wave.stroke} strokeWidth="1.5" strokeLinecap="round" />
|
||||||
|
</svg>
|
||||||
|
<div className="file-item-content">
|
||||||
|
<span className="file-label">
|
||||||
|
<span className="file-label-primary">{primaryLabel}</span>
|
||||||
|
<span className="file-label-secondary">{stripExtension(file.fileName)}</span>
|
||||||
|
</span>
|
||||||
|
<span className="file-meta">{formatDuration(file.durationMs)}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{files.length === 0 && <div className="file-empty">No files matched the current filters.</div>}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const WAVEFORM_WIDTH = 240;
|
||||||
|
const WAVEFORM_HEIGHT = 68;
|
||||||
|
const WAVEFORM_POINT_COUNT = 160;
|
||||||
|
|
||||||
|
const RMS_DB_MIN = -24;
|
||||||
|
const RMS_DB_MAX = 0;
|
||||||
|
const MIN_RMS_VALUE = 1e-4;
|
||||||
|
|
||||||
|
const RMS_COLOR_STOPS: ReadonlyArray<{ value: number; hue: number; saturation: number; lightness: number }> = [
|
||||||
|
{ value: 0, hue: 210, saturation: 70, lightness: 56 },
|
||||||
|
{ value: 0.33, hue: 150, saturation: 72, lightness: 52 },
|
||||||
|
{ value: 0.66, hue: 48, saturation: 76, lightness: 50 },
|
||||||
|
{ value: 1, hue: 8, saturation: 82, lightness: 46 }
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
interface WaveformVisual {
|
||||||
|
path: string;
|
||||||
|
stroke: string;
|
||||||
|
gradientStart: string;
|
||||||
|
gradientEnd: string;
|
||||||
|
rms: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWaveformVisual(seedValue: string, samples: number[] | null, rms: number | null): WaveformVisual {
|
||||||
|
const dataset = samples && samples.length > 1 ? samples : createFallbackSamples(seedValue);
|
||||||
|
const resolvedRms = clampRms(typeof rms === 'number' ? rms : computeRms(dataset));
|
||||||
|
const colorLevel = mapRmsToColorLevel(resolvedRms);
|
||||||
|
const palette = deriveWaveformPalette(seedValue, colorLevel);
|
||||||
|
const path = buildWaveformPath(dataset);
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
stroke: palette.stroke,
|
||||||
|
gradientStart: palette.gradientStart,
|
||||||
|
gradientEnd: palette.gradientEnd,
|
||||||
|
rms: resolvedRms
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildWaveformPath(samples: number[]): string {
|
||||||
|
if (samples.length === 0) {
|
||||||
|
const baseline = WAVEFORM_HEIGHT / 2;
|
||||||
|
return `M 0 ${baseline.toFixed(2)} L ${WAVEFORM_WIDTH.toFixed(2)} ${baseline.toFixed(2)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseline = WAVEFORM_HEIGHT / 2;
|
||||||
|
const amplitudeScale = WAVEFORM_HEIGHT * 0.48;
|
||||||
|
const step = samples.length > 1 ? WAVEFORM_WIDTH / (samples.length - 1) : WAVEFORM_WIDTH;
|
||||||
|
const topSegments: string[] = [];
|
||||||
|
const bottomSegments: string[] = [];
|
||||||
|
|
||||||
|
for (let index = 0; index < samples.length; index += 1) {
|
||||||
|
const sample = Math.max(0, Math.min(1, samples[index] ?? 0));
|
||||||
|
const x = step * index;
|
||||||
|
const amplitude = sample * amplitudeScale;
|
||||||
|
const topY = baseline - amplitude;
|
||||||
|
const bottomY = baseline + amplitude;
|
||||||
|
topSegments.push(`L ${x.toFixed(2)} ${topY.toFixed(2)}`);
|
||||||
|
bottomSegments.push(`L ${x.toFixed(2)} ${bottomY.toFixed(2)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = `M 0 ${baseline.toFixed(2)}`;
|
||||||
|
if (topSegments.length > 0) {
|
||||||
|
path += ` ${topSegments.join(' ')}`;
|
||||||
|
}
|
||||||
|
if (bottomSegments.length > 0) {
|
||||||
|
for (let index = bottomSegments.length - 1; index >= 0; index -= 1) {
|
||||||
|
path += ` ${bottomSegments[index]}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path += ' Z';
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFallbackSamples(seedValue: string): number[] {
|
||||||
|
const rng = mulberry32(hashString(`${seedValue}-fallback`));
|
||||||
|
const samples: number[] = [];
|
||||||
|
for (let index = 0; index < WAVEFORM_POINT_COUNT; index += 1) {
|
||||||
|
const t = index / Math.max(WAVEFORM_POINT_COUNT - 1, 1);
|
||||||
|
const envelope = Math.sin(t * Math.PI);
|
||||||
|
const jitter = (rng() - 0.5) * 0.25;
|
||||||
|
const value = Math.min(1, Math.max(0, 0.18 + envelope * (0.55 + jitter)));
|
||||||
|
samples.push(value);
|
||||||
|
}
|
||||||
|
return samples;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deriveWaveformPalette(seedValue: string, level: number): Pick<WaveformVisual, 'gradientStart' | 'gradientEnd' | 'stroke'> {
|
||||||
|
const normalized = clamp(level, 0, 1);
|
||||||
|
const baseColor = interpolateColorStops(RMS_COLOR_STOPS, normalized);
|
||||||
|
const jitterSeed = hashString(`${seedValue}-palette`);
|
||||||
|
const hueJitter = ((jitterSeed % 19) - 9) * 0.6;
|
||||||
|
const saturationJitter = (((jitterSeed >> 4) % 17) - 8) * 0.5;
|
||||||
|
const lightnessJitter = (((jitterSeed >> 9) % 15) - 7) * 0.6;
|
||||||
|
|
||||||
|
const baseHue = wrapHue(baseColor.hue + hueJitter);
|
||||||
|
const saturation = clamp(baseColor.saturation + saturationJitter, 42, 90);
|
||||||
|
const baseLightness = clamp(baseColor.lightness + lightnessJitter, 28, 64);
|
||||||
|
const accentHue = wrapHue(baseHue + 18);
|
||||||
|
const strokeHue = wrapHue(baseHue + 8);
|
||||||
|
const gradientStartLightness = clamp(baseLightness + 6, 32, 74);
|
||||||
|
const gradientEndLightness = clamp(baseLightness - 6, 20, 60);
|
||||||
|
const strokeLightness = clamp(baseLightness - 10, 18, 58);
|
||||||
|
const strokeAlpha = clamp(0.44 + normalized * 0.4, 0.32, 0.86);
|
||||||
|
|
||||||
|
return {
|
||||||
|
gradientStart: `hsla(${Math.round(baseHue)}, ${Math.round(saturation)}%, ${Math.round(gradientStartLightness)}%, 0.32)`,
|
||||||
|
gradientEnd: `hsla(${Math.round(accentHue)}, ${Math.round(saturation + 6)}%, ${Math.round(gradientEndLightness)}%, 0.24)`,
|
||||||
|
stroke: `hsla(${Math.round(strokeHue)}, ${Math.round(saturation + 4)}%, ${Math.round(strokeLightness)}%, ${strokeAlpha.toFixed(2)})`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts an RMS value into a 0..1 interpolation level using a decibel scale.
|
||||||
|
*/
|
||||||
|
function mapRmsToColorLevel(rms: number): number {
|
||||||
|
if (!Number.isFinite(rms) || rms <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const safeRms = clamp(rms, MIN_RMS_VALUE, 1);
|
||||||
|
const db = 20 * Math.log10(safeRms);
|
||||||
|
const clampedDb = clamp(db, RMS_DB_MIN, RMS_DB_MAX);
|
||||||
|
return (clampedDb - RMS_DB_MIN) / (RMS_DB_MAX - RMS_DB_MIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs linear interpolation across color stops for the provided interpolation value (0..1).
|
||||||
|
*/
|
||||||
|
function interpolateColorStops(
|
||||||
|
stops: ReadonlyArray<{ value: number; hue: number; saturation: number; lightness: number }>,
|
||||||
|
value: number
|
||||||
|
): { hue: number; saturation: number; lightness: number } {
|
||||||
|
if (stops.length === 0) {
|
||||||
|
return { hue: 0, saturation: 60, lightness: 50 };
|
||||||
|
}
|
||||||
|
if (value <= stops[0].value) {
|
||||||
|
const { hue, saturation, lightness } = stops[0];
|
||||||
|
return { hue, saturation, lightness };
|
||||||
|
}
|
||||||
|
if (value >= stops[stops.length - 1].value) {
|
||||||
|
const { hue, saturation, lightness } = stops[stops.length - 1];
|
||||||
|
return { hue, saturation, lightness };
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < stops.length - 1; index += 1) {
|
||||||
|
const start = stops[index];
|
||||||
|
const end = stops[index + 1];
|
||||||
|
if (value >= start.value && value <= end.value) {
|
||||||
|
const range = end.value - start.value;
|
||||||
|
const t = range > 0 ? (value - start.value) / range : 0;
|
||||||
|
return {
|
||||||
|
hue: start.hue + (end.hue - start.hue) * t,
|
||||||
|
saturation: start.saturation + (end.saturation - start.saturation) * t,
|
||||||
|
lightness: start.lightness + (end.lightness - start.lightness) * t
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const { hue, saturation, lightness } = stops[stops.length - 1];
|
||||||
|
return { hue, saturation, lightness };
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeRms(samples: number[]): number {
|
||||||
|
if (samples.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let sumSquares = 0;
|
||||||
|
for (let index = 0; index < samples.length; index += 1) {
|
||||||
|
const value = clamp(samples[index] ?? 0, 0, 1);
|
||||||
|
sumSquares += value * value;
|
||||||
|
}
|
||||||
|
return Math.sqrt(sumSquares / samples.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampRms(value: number): number {
|
||||||
|
if (!Number.isFinite(value)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return clamp(value, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(value: number, min: number, max: number): number {
|
||||||
|
return Math.min(max, Math.max(min, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapHue(value: number): number {
|
||||||
|
const modulo = value % 360;
|
||||||
|
return modulo < 0 ? modulo + 360 : modulo;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hashString(value: string): number {
|
||||||
|
let hash = 2166136261;
|
||||||
|
for (let index = 0; index < value.length; index += 1) {
|
||||||
|
hash ^= value.charCodeAt(index);
|
||||||
|
hash = Math.imul(hash, 16777619);
|
||||||
|
}
|
||||||
|
return hash >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mulberry32(seed: number): () => number {
|
||||||
|
return () => {
|
||||||
|
let t = (seed += 0x6d2b79f5);
|
||||||
|
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||||
|
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||||
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDuration(durationMs: number | null): string {
|
||||||
|
if (!durationMs || Number.isNaN(durationMs)) {
|
||||||
|
return '–';
|
||||||
|
}
|
||||||
|
const seconds = Math.round(durationMs / 1000);
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const remaining = seconds % 60;
|
||||||
|
return `${minutes}:${remaining.toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBytes(bytes: number): string {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return `${(bytes / Math.pow(k, i)).toFixed(1)} ${sizes[i]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileList;
|
||||||
410
src/renderer/src/components/MultiFileEditor.tsx
Normal file
410
src/renderer/src/components/MultiFileEditor.tsx
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
import { useState, useEffect, useRef, useLayoutEffect, useMemo, useCallback, type UIEvent } from 'react';
|
||||||
|
import type { AudioFileSummary, CategoryRecord } from '../../../shared/models';
|
||||||
|
import { getMultiEditorScrollTop, setMultiEditorScrollTop } from '../stores/CategoryUIState';
|
||||||
|
import { TagEditor } from './TagEditor';
|
||||||
|
|
||||||
|
export interface MultiFileEditorProps {
|
||||||
|
files: AudioFileSummary[];
|
||||||
|
categories: CategoryRecord[];
|
||||||
|
onUpdateTags(fileId: number, data: { tags: string[]; categories: string[] }): Promise<void>;
|
||||||
|
onUpdateCustomName(fileId: number, customName: string | null): Promise<void>;
|
||||||
|
onOrganize(fileId: number, metadata: { customName?: string | null; author?: string | null; copyright?: string | null; rating?: number }): Promise<void>;
|
||||||
|
onUpdateMetadata(fileId: number, metadata: { author?: string | null; copyright?: string | null; rating?: number }): Promise<void>;
|
||||||
|
metadataSuggestionsVersion: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multi-file tag editor that shows aggregated metadata and allows batch editing.
|
||||||
|
*/
|
||||||
|
export function MultiFileEditor({ files, categories, onUpdateTags, onUpdateCustomName, onOrganize, onUpdateMetadata, metadataSuggestionsVersion }: MultiFileEditorProps): JSX.Element {
|
||||||
|
const [sharedCustomName, setSharedCustomName] = useState('');
|
||||||
|
const [sharedAuthor, setSharedAuthor] = useState('');
|
||||||
|
const [sharedCopyright, setSharedCopyright] = useState('');
|
||||||
|
const [sharedRating, setSharedRating] = useState(0);
|
||||||
|
const [suggestions, setSuggestions] = useState<{ authors: string[]; copyrights: string[] }>({ authors: [], copyrights: [] });
|
||||||
|
const [isTagSectionExpanded, setIsTagSectionExpanded] = useState(true);
|
||||||
|
|
||||||
|
// Compute aggregated tags and categories for TagEditor
|
||||||
|
const aggregatedTags = useMemo(() => {
|
||||||
|
const tagCounts = new Map<string, number>();
|
||||||
|
for (const file of files) {
|
||||||
|
for (const tag of file.tags) {
|
||||||
|
tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Only show tags that appear in all files
|
||||||
|
return Array.from(tagCounts.entries())
|
||||||
|
.filter(([, count]) => count === files.length)
|
||||||
|
.map(([tag]) => tag);
|
||||||
|
}, [files]);
|
||||||
|
|
||||||
|
const aggregatedCategories = useMemo(() => {
|
||||||
|
const categoryCounts = new Map<string, number>();
|
||||||
|
for (const file of files) {
|
||||||
|
for (const category of file.categories) {
|
||||||
|
categoryCounts.set(category, (categoryCounts.get(category) || 0) + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Only show categories that appear in all files
|
||||||
|
return Array.from(categoryCounts.entries())
|
||||||
|
.filter(([, count]) => count === files.length)
|
||||||
|
.map(([category]) => category);
|
||||||
|
}, [files]);
|
||||||
|
|
||||||
|
const listRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
type SuggestionKey = 'authors' | 'copyrights';
|
||||||
|
|
||||||
|
const appendSuggestion = useCallback((value: string, key: SuggestionKey) => {
|
||||||
|
const trimmed = value.trim();
|
||||||
|
if (trimmed.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSuggestions((previous) => {
|
||||||
|
if (previous[key].some((entry) => entry.localeCompare(trimmed, undefined, { sensitivity: 'accent' }) === 0)) {
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
const nextList = [...previous[key], trimmed].sort((a, b) => a.localeCompare(b));
|
||||||
|
return {
|
||||||
|
...previous,
|
||||||
|
[key]: nextList
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const filteredAuthorSuggestions = useMemo(() => {
|
||||||
|
if (suggestions.authors.length === 0) {
|
||||||
|
return [] as string[];
|
||||||
|
}
|
||||||
|
const query = sharedAuthor.trim().toLowerCase();
|
||||||
|
const base = suggestions.authors.filter((entry) => entry.toLowerCase() !== query);
|
||||||
|
if (query.length === 0) {
|
||||||
|
return base.slice(0, 5);
|
||||||
|
}
|
||||||
|
return base.filter((entry) => entry.toLowerCase().includes(query)).slice(0, 5);
|
||||||
|
}, [sharedAuthor, suggestions.authors]);
|
||||||
|
|
||||||
|
const filteredCopyrightSuggestions = useMemo(() => {
|
||||||
|
if (suggestions.copyrights.length === 0) {
|
||||||
|
return [] as string[];
|
||||||
|
}
|
||||||
|
const query = sharedCopyright.trim().toLowerCase();
|
||||||
|
const base = suggestions.copyrights.filter((entry) => entry.toLowerCase() !== query);
|
||||||
|
if (query.length === 0) {
|
||||||
|
return base.slice(0, 5);
|
||||||
|
}
|
||||||
|
return base.filter((entry) => entry.toLowerCase().includes(query)).slice(0, 5);
|
||||||
|
}, [sharedCopyright, suggestions.copyrights]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
setSuggestions({ authors: [], copyrights: [] });
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
const result = await window.api.listMetadataSuggestions();
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSuggestions({
|
||||||
|
authors: result.authors,
|
||||||
|
copyrights: result.copyrights
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (!cancelled) {
|
||||||
|
console.error('Failed to load metadata suggestions:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [metadataSuggestionsVersion]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const node = listRef.current;
|
||||||
|
if (node) {
|
||||||
|
node.scrollTop = getMultiEditorScrollTop();
|
||||||
|
}
|
||||||
|
}, [files]);
|
||||||
|
|
||||||
|
const handleScroll = (event: UIEvent<HTMLDivElement>) => {
|
||||||
|
setMultiEditorScrollTop(event.currentTarget.scrollTop);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSharedCustomNameBlur = async () => {
|
||||||
|
const normalized = sharedCustomName.trim().length > 0 ? sharedCustomName.trim() : null;
|
||||||
|
|
||||||
|
if (!normalized) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let failureCount = 0;
|
||||||
|
for (const file of files) {
|
||||||
|
try {
|
||||||
|
if (file.categories.length > 0) {
|
||||||
|
await onOrganize(file.id, { customName: normalized });
|
||||||
|
} else {
|
||||||
|
await onUpdateCustomName(file.id, normalized);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
failureCount += 1;
|
||||||
|
console.error(`Failed to update custom name for file ${file.id} (${file.fileName}):`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (failureCount > 0) {
|
||||||
|
console.error(`${failureCount} file(s) failed to update`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update custom names:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSharedAuthorBlur = async () => {
|
||||||
|
const normalized = sharedAuthor.trim();
|
||||||
|
const authorPayload = normalized.length > 0 ? normalized : null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let failureCount = 0;
|
||||||
|
for (const file of files) {
|
||||||
|
try {
|
||||||
|
if (file.categories.length > 0) {
|
||||||
|
await onOrganize(file.id, { author: authorPayload });
|
||||||
|
} else {
|
||||||
|
await onUpdateMetadata(file.id, { author: authorPayload });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
failureCount += 1;
|
||||||
|
console.error(`Failed to update author for file ${file.id} (${file.fileName}):`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (failureCount > 0) {
|
||||||
|
console.error(`${failureCount} file(s) failed to update author`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authorPayload && authorPayload.length > 0) {
|
||||||
|
appendSuggestion(authorPayload, 'authors');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update author:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSharedCopyrightBlur = async () => {
|
||||||
|
const normalized = sharedCopyright.trim();
|
||||||
|
|
||||||
|
if (normalized.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let failureCount = 0;
|
||||||
|
for (const file of files) {
|
||||||
|
try {
|
||||||
|
if (file.categories.length > 0) {
|
||||||
|
await onOrganize(file.id, { copyright: normalized });
|
||||||
|
} else {
|
||||||
|
await onUpdateMetadata(file.id, { copyright: normalized });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
failureCount += 1;
|
||||||
|
console.error(`Failed to update copyright for file ${file.id} (${file.fileName}):`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (failureCount > 0) {
|
||||||
|
console.error(`${failureCount} file(s) failed to update copyright`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.length > 0) {
|
||||||
|
appendSuggestion(normalized, 'copyrights');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update copyright:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSharedRatingChange = async (rating: number) => {
|
||||||
|
setSharedRating(rating);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let failureCount = 0;
|
||||||
|
for (const file of files) {
|
||||||
|
try {
|
||||||
|
if (file.categories.length > 0) {
|
||||||
|
await onOrganize(file.id, { rating });
|
||||||
|
} else {
|
||||||
|
await onUpdateMetadata(file.id, { rating });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
failureCount += 1;
|
||||||
|
console.error(`Failed to update rating for file ${file.id} (${file.fileName}):`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (failureCount > 0) {
|
||||||
|
console.error(`${failureCount} file(s) failed to update rating`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update rating:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTagSave = async (data: { tags: string[]; categories: string[] }) => {
|
||||||
|
try {
|
||||||
|
let failureCount = 0;
|
||||||
|
for (const file of files) {
|
||||||
|
try {
|
||||||
|
await onUpdateTags(file.id, data);
|
||||||
|
} catch (error) {
|
||||||
|
failureCount += 1;
|
||||||
|
console.error(`Failed to update tags for file ${file.id} (${file.fileName}):`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (failureCount > 0) {
|
||||||
|
console.error(`${failureCount} file(s) failed to update tags`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update tags:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleTagSection = () => {
|
||||||
|
setIsTagSectionExpanded((value) => !value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="multi-file-editor">
|
||||||
|
<header className="multi-file-editor-header">
|
||||||
|
<h2>Editing {files.length} files</h2>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="multi-file-editor-body" ref={listRef} onScroll={handleScroll}>
|
||||||
|
<div className="multi-file-metadata-section">
|
||||||
|
<h3>Shared Metadata</h3>
|
||||||
|
<div className="multi-file-metadata-grid">
|
||||||
|
<label>
|
||||||
|
<span>Name</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={sharedCustomName}
|
||||||
|
onChange={(e) => setSharedCustomName(e.target.value)}
|
||||||
|
onBlur={handleSharedCustomNameBlur}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.currentTarget.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="Custom name for all selected files"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Author</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={sharedAuthor}
|
||||||
|
onChange={(e) => setSharedAuthor(e.target.value)}
|
||||||
|
onBlur={handleSharedAuthorBlur}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.currentTarget.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="Artist or creator name"
|
||||||
|
/>
|
||||||
|
{filteredAuthorSuggestions.length > 0 && (
|
||||||
|
<div className="metadata-suggestion-list" role="listbox" aria-label="Author suggestions">
|
||||||
|
{filteredAuthorSuggestions.map((option) => (
|
||||||
|
<button
|
||||||
|
key={option}
|
||||||
|
type="button"
|
||||||
|
className="metadata-suggestion"
|
||||||
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
|
onClick={() => {
|
||||||
|
setSharedAuthor(option);
|
||||||
|
void handleSharedAuthorBlur();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{option}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Copyright</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={sharedCopyright}
|
||||||
|
onChange={(e) => setSharedCopyright(e.target.value)}
|
||||||
|
onBlur={handleSharedCopyrightBlur}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.currentTarget.blur();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="Copyright information"
|
||||||
|
/>
|
||||||
|
{filteredCopyrightSuggestions.length > 0 && (
|
||||||
|
<div className="metadata-suggestion-list" role="listbox" aria-label="Copyright suggestions">
|
||||||
|
{filteredCopyrightSuggestions.map((option) => (
|
||||||
|
<button
|
||||||
|
key={option}
|
||||||
|
type="button"
|
||||||
|
className="metadata-suggestion"
|
||||||
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
|
onClick={() => {
|
||||||
|
setSharedCopyright(option);
|
||||||
|
void handleSharedCopyrightBlur();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{option}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Rating</span>
|
||||||
|
<div className="star-rating">
|
||||||
|
{[1, 2, 3, 4, 5].map((star) => (
|
||||||
|
<button
|
||||||
|
key={star}
|
||||||
|
type="button"
|
||||||
|
className={sharedRating >= star ? 'star star--filled' : 'star'}
|
||||||
|
onClick={() => {
|
||||||
|
const nextValue = sharedRating === star ? 0 : star;
|
||||||
|
void handleSharedRatingChange(nextValue);
|
||||||
|
}}
|
||||||
|
aria-label={`${star} star${star > 1 ? 's' : ''}`}
|
||||||
|
>
|
||||||
|
★
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="tag-editor-pane">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="tag-editor-toggle"
|
||||||
|
onClick={toggleTagSection}
|
||||||
|
aria-expanded={isTagSectionExpanded}
|
||||||
|
>
|
||||||
|
<span className="tag-editor-toggle-label">Tags</span>
|
||||||
|
<span className="tag-editor-toggle-icon" aria-hidden="true">
|
||||||
|
{isTagSectionExpanded ? 'v' : '>'}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<div className={isTagSectionExpanded ? 'tag-editor-content tag-editor-content--open' : 'tag-editor-content'} aria-hidden={!isTagSectionExpanded}>
|
||||||
|
<TagEditor
|
||||||
|
tags={aggregatedTags}
|
||||||
|
categories={aggregatedCategories}
|
||||||
|
availableCategories={categories}
|
||||||
|
onSave={handleTagSave}
|
||||||
|
showHeading={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MultiFileEditor;
|
||||||
215
src/renderer/src/components/SearchBar.tsx
Normal file
215
src/renderer/src/components/SearchBar.tsx
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
type ChangeEvent,
|
||||||
|
type SyntheticEvent
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
type SuggestionType = 'author' | 'copyright';
|
||||||
|
|
||||||
|
interface MetadataSuggestionPayload {
|
||||||
|
authors: string[];
|
||||||
|
copyrights: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchBarProps {
|
||||||
|
value: string;
|
||||||
|
onChange(value: string): void;
|
||||||
|
onRescan(): Promise<void>;
|
||||||
|
onFindDuplicates(): void;
|
||||||
|
onOpenSettings(): void;
|
||||||
|
metadataSuggestionsVersion: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Top level search bar with quick access to rescanning and settings.
|
||||||
|
*/
|
||||||
|
export function SearchBar({ value, onChange, onRescan, onFindDuplicates, onOpenSettings, metadataSuggestionsVersion }: SearchBarProps): JSX.Element {
|
||||||
|
const [draft, setDraft] = useState(value);
|
||||||
|
const [caretIndex, setCaretIndex] = useState<number | null>(null);
|
||||||
|
const [suggestions, setSuggestions] = useState<MetadataSuggestionPayload>({ authors: [], copyrights: [] });
|
||||||
|
const [loadingSuggestions, setLoadingSuggestions] = useState(false);
|
||||||
|
const [rescanBusy, setRescanBusy] = useState(false);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDraft(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
const refreshSuggestions = useCallback(async () => {
|
||||||
|
setLoadingSuggestions(true);
|
||||||
|
try {
|
||||||
|
const result = await window.api.listMetadataSuggestions();
|
||||||
|
setSuggestions({
|
||||||
|
authors: result.authors,
|
||||||
|
copyrights: result.copyrights
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load search suggestions', error);
|
||||||
|
} finally {
|
||||||
|
setLoadingSuggestions(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setSuggestions({ authors: [], copyrights: [] });
|
||||||
|
void refreshSuggestions();
|
||||||
|
}, [refreshSuggestions, metadataSuggestionsVersion]);
|
||||||
|
|
||||||
|
const analysisTarget = useMemo(() => {
|
||||||
|
const cursor = caretIndex ?? draft.length;
|
||||||
|
return draft.slice(0, cursor);
|
||||||
|
}, [caretIndex, draft]);
|
||||||
|
|
||||||
|
const activeToken = useMemo(() => {
|
||||||
|
const match = analysisTarget.match(/(author|copyright):([^\s]*)$/i);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const type = match[1].toLowerCase() as SuggestionType;
|
||||||
|
return {
|
||||||
|
type,
|
||||||
|
term: match[2] ?? ''
|
||||||
|
};
|
||||||
|
}, [analysisTarget]);
|
||||||
|
|
||||||
|
const filteredSuggestions = useMemo(() => {
|
||||||
|
if (!activeToken) {
|
||||||
|
return [] as string[];
|
||||||
|
}
|
||||||
|
const pool = activeToken.type === 'author' ? suggestions.authors : suggestions.copyrights;
|
||||||
|
const trimmedTerm = activeToken.term.trim();
|
||||||
|
if (trimmedTerm.length === 0) {
|
||||||
|
return pool.slice(0, 6);
|
||||||
|
}
|
||||||
|
const needle = trimmedTerm.toLowerCase();
|
||||||
|
return pool
|
||||||
|
.filter((entry) => entry.toLowerCase().includes(needle))
|
||||||
|
.slice(0, 6);
|
||||||
|
}, [activeToken, suggestions]);
|
||||||
|
|
||||||
|
const activeSuggestions = useMemo(() => {
|
||||||
|
if (!activeToken) {
|
||||||
|
return [] as Array<{ type: SuggestionType; value: string }>;
|
||||||
|
}
|
||||||
|
return filteredSuggestions.map((value) => ({ type: activeToken.type, value }));
|
||||||
|
}, [activeToken, filteredSuggestions]);
|
||||||
|
|
||||||
|
const showSuggestions = activeToken !== null && activeSuggestions.length > 0;
|
||||||
|
const showLoading = loadingSuggestions && activeToken !== null;
|
||||||
|
|
||||||
|
const handleCaretUpdate = (event: SyntheticEvent<HTMLInputElement>) => {
|
||||||
|
const target = event.currentTarget;
|
||||||
|
setCaretIndex(target.selectionStart ?? target.value.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const nextValue = event.target.value;
|
||||||
|
setDraft(nextValue);
|
||||||
|
setCaretIndex(event.target.selectionStart ?? nextValue.length);
|
||||||
|
onChange(nextValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const applySuggestion = (type: SuggestionType, suggestionValue: string) => {
|
||||||
|
const previousValue = inputRef.current?.value ?? draft;
|
||||||
|
const cursor = caretIndex ?? previousValue.length;
|
||||||
|
const head = previousValue.slice(0, cursor);
|
||||||
|
const tail = previousValue.slice(cursor);
|
||||||
|
const pattern = new RegExp(`(${type}:)[^\s]*$`, 'i');
|
||||||
|
let nextHead: string;
|
||||||
|
if (pattern.test(head)) {
|
||||||
|
nextHead = head.replace(pattern, `${type}:${suggestionValue}`);
|
||||||
|
} else {
|
||||||
|
const prefix = head.length === 0 || /\s$/.test(head) ? '' : ' ';
|
||||||
|
nextHead = `${head}${prefix}${type}:${suggestionValue}`;
|
||||||
|
}
|
||||||
|
let nextValue = `${nextHead}${tail}`;
|
||||||
|
let nextCursor = nextHead.length;
|
||||||
|
if (tail.length > 0 && !/^\s/.test(tail)) {
|
||||||
|
nextValue = `${nextHead} ${tail}`;
|
||||||
|
nextCursor += 1;
|
||||||
|
}
|
||||||
|
setDraft(nextValue);
|
||||||
|
setCaretIndex(nextCursor);
|
||||||
|
onChange(nextValue);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const node = inputRef.current;
|
||||||
|
if (node) {
|
||||||
|
node.focus();
|
||||||
|
node.setSelectionRange(nextCursor, nextCursor);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSuggestionClick = (type: SuggestionType, suggestionValue: string) => {
|
||||||
|
applySuggestion(type, suggestionValue);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputFocus = () => {
|
||||||
|
if (!loadingSuggestions) {
|
||||||
|
void refreshSuggestions();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRescanClick = async () => {
|
||||||
|
setRescanBusy(true);
|
||||||
|
try {
|
||||||
|
await onRescan();
|
||||||
|
await refreshSuggestions();
|
||||||
|
} finally {
|
||||||
|
setRescanBusy(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="search-bar">
|
||||||
|
<div className="search-input-wrapper">
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
className="search-input"
|
||||||
|
placeholder="Search by name, tag, category, or author:"
|
||||||
|
value={draft}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
onSelect={handleCaretUpdate}
|
||||||
|
onKeyUp={handleCaretUpdate}
|
||||||
|
onFocus={handleInputFocus}
|
||||||
|
/>
|
||||||
|
<div className="search-suggestions" aria-live="polite">
|
||||||
|
{showLoading && <span className="search-suggestions__status">Loading suggestions…</span>}
|
||||||
|
{showSuggestions && (
|
||||||
|
<span className="search-suggestions__label">Suggestions:</span>
|
||||||
|
)}
|
||||||
|
{showSuggestions &&
|
||||||
|
activeSuggestions.map((suggestion) => (
|
||||||
|
<button
|
||||||
|
key={`${suggestion.type}:${suggestion.value}`}
|
||||||
|
type="button"
|
||||||
|
className="search-suggestion"
|
||||||
|
onMouseDown={(event) => event.preventDefault()}
|
||||||
|
onClick={() => handleSuggestionClick(suggestion.type, suggestion.value)}
|
||||||
|
>
|
||||||
|
<span className="search-suggestion__type">{suggestion.type}:</span>
|
||||||
|
<span className="search-suggestion__value">{suggestion.value}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="search-actions">
|
||||||
|
<button type="button" onClick={handleRescanClick} className="ghost-button" disabled={rescanBusy}>
|
||||||
|
{rescanBusy ? 'Rescanning…' : 'Rescan'}
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={onFindDuplicates} className="ghost-button">
|
||||||
|
Find Duplicates
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={onOpenSettings} className="ghost-button">
|
||||||
|
Settings
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchBar;
|
||||||
67
src/renderer/src/components/SettingsDialog.tsx
Normal file
67
src/renderer/src/components/SettingsDialog.tsx
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { useEffect, useState, type ChangeEvent } from 'react';
|
||||||
|
|
||||||
|
export interface SettingsDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
currentPath: string | null;
|
||||||
|
onClose(): void;
|
||||||
|
onSelectDirectory(path: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modal dialog for selecting or typing a new library path.
|
||||||
|
*/
|
||||||
|
export function SettingsDialog({ open, currentPath, onClose, onSelectDirectory }: SettingsDialogProps): JSX.Element | null {
|
||||||
|
const [draftPath, setDraftPath] = useState(currentPath ?? '');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDraftPath(currentPath ?? '');
|
||||||
|
}, [currentPath]);
|
||||||
|
|
||||||
|
if (!open) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pickDirectory = async () => {
|
||||||
|
const directory = await window.api.selectLibraryDirectory();
|
||||||
|
if (directory) {
|
||||||
|
setDraftPath(directory);
|
||||||
|
onSelectDirectory(directory);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = () => {
|
||||||
|
if (draftPath.trim().length > 0) {
|
||||||
|
onSelectDirectory(draftPath.trim());
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dialog-backdrop" role="dialog" aria-modal="true">
|
||||||
|
<div className="dialog">
|
||||||
|
<h2>Library Settings</h2>
|
||||||
|
<label className="dialog-section">
|
||||||
|
<span>Library Location</span>
|
||||||
|
<input
|
||||||
|
value={draftPath}
|
||||||
|
onChange={(event: ChangeEvent<HTMLInputElement>) => setDraftPath(event.target.value)}
|
||||||
|
placeholder="Choose where your WAV library lives"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div className="dialog-actions">
|
||||||
|
<button type="button" className="ghost-button" onClick={pickDirectory}>
|
||||||
|
Browse…
|
||||||
|
</button>
|
||||||
|
<button type="button" className="primary-button" onClick={save}>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
<button type="button" className="ghost-button" onClick={onClose}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SettingsDialog;
|
||||||
341
src/renderer/src/components/TagEditor.tsx
Normal file
341
src/renderer/src/components/TagEditor.tsx
Normal file
@@ -0,0 +1,341 @@
|
|||||||
|
import {
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
useRef,
|
||||||
|
useLayoutEffect,
|
||||||
|
type ChangeEvent,
|
||||||
|
type UIEvent,
|
||||||
|
type CSSProperties
|
||||||
|
} from 'react';
|
||||||
|
import type { CategoryRecord } from '../../../shared/models';
|
||||||
|
import {
|
||||||
|
getCollapsedGroups,
|
||||||
|
initializeCollapsedGroups,
|
||||||
|
setCollapsedGroups as setModuleCollapsedGroups,
|
||||||
|
getTagEditorScrollTop,
|
||||||
|
setTagEditorScrollTop
|
||||||
|
} from '../stores/CategoryUIState';
|
||||||
|
import {
|
||||||
|
buildCategorySwatch,
|
||||||
|
createCategoryStyleVars,
|
||||||
|
formatCategoryLabel
|
||||||
|
} from '../utils/categoryColors';
|
||||||
|
import type { CategorySwatch } from '../utils/categoryColors';
|
||||||
|
|
||||||
|
export interface TagEditorProps {
|
||||||
|
tags: string[];
|
||||||
|
categories: string[];
|
||||||
|
availableCategories: CategoryRecord[];
|
||||||
|
onSave(data: { tags: string[]; categories: string[] }): void;
|
||||||
|
/** If false the internal heading is omitted. Defaults to true. */
|
||||||
|
showHeading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows editing free-form tags and selecting UCS categories.
|
||||||
|
*/
|
||||||
|
export function TagEditor({ tags, categories, availableCategories, onSave, showHeading = true }: TagEditorProps): JSX.Element {
|
||||||
|
const [tagDraft, setTagDraft] = useState(tags.join(', '));
|
||||||
|
const [categoryFilter, setCategoryFilter] = useState('');
|
||||||
|
const [selectedCategories, setSelectedCategories] = useState(new Set(categories));
|
||||||
|
const [isCategoryListExpanded, setIsCategoryListExpanded] = useState(false);
|
||||||
|
|
||||||
|
// Initialize module state if needed
|
||||||
|
initializeCollapsedGroups(availableCategories.map((cat) => cat.category));
|
||||||
|
|
||||||
|
const [collapsedGroups, setCollapsedGroups] = useState(() => {
|
||||||
|
const moduleState = getCollapsedGroups();
|
||||||
|
return new Set(moduleState!);
|
||||||
|
});
|
||||||
|
const listRef = useRef<HTMLDivElement>(null);
|
||||||
|
const isFirstRender = useRef(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTagDraft(tags.join(', '));
|
||||||
|
setSelectedCategories(new Set(categories));
|
||||||
|
|
||||||
|
// Sync collapsed state from module state when file changes (but not on first render)
|
||||||
|
if (!isFirstRender.current) {
|
||||||
|
const moduleState = getCollapsedGroups();
|
||||||
|
console.log('TagEditor syncing collapsed state from module:', Array.from(moduleState!));
|
||||||
|
setCollapsedGroups(new Set(moduleState!));
|
||||||
|
} else {
|
||||||
|
isFirstRender.current = false;
|
||||||
|
}
|
||||||
|
}, [tags, categories]);
|
||||||
|
|
||||||
|
// No need for this effect anymore - categories don't change dynamically
|
||||||
|
|
||||||
|
const { groupedCategories, filteredResults } = useMemo(() => {
|
||||||
|
const filter = categoryFilter.trim().toLowerCase();
|
||||||
|
const filtered = filter.length === 0
|
||||||
|
? availableCategories
|
||||||
|
: availableCategories.filter((entry: CategoryRecord) => {
|
||||||
|
const haystack = `${entry.category} ${entry.subCategory} ${entry.shortCode} ${entry.synonyms.join(' ')}`.toLowerCase();
|
||||||
|
return haystack.includes(filter);
|
||||||
|
});
|
||||||
|
|
||||||
|
const grouped = filtered.reduce((acc, category) => {
|
||||||
|
if (!acc[category.category]) {
|
||||||
|
acc[category.category] = [];
|
||||||
|
}
|
||||||
|
acc[category.category].push(category);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, CategoryRecord[]>);
|
||||||
|
return { groupedCategories: grouped, filteredResults: filtered };
|
||||||
|
}, [availableCategories, categoryFilter]);
|
||||||
|
|
||||||
|
const selectedCategoryRecords = useMemo(
|
||||||
|
() => availableCategories
|
||||||
|
.filter((entry) => selectedCategories.has(entry.id))
|
||||||
|
.sort((left, right) => {
|
||||||
|
const topLevelComparison = left.category.localeCompare(right.category);
|
||||||
|
if (topLevelComparison !== 0) {
|
||||||
|
return topLevelComparison;
|
||||||
|
}
|
||||||
|
return left.subCategory.localeCompare(right.subCategory);
|
||||||
|
}),
|
||||||
|
[availableCategories, selectedCategories]
|
||||||
|
);
|
||||||
|
|
||||||
|
const categoryColorMap = useMemo(() => {
|
||||||
|
const map = new Map<string, CategorySwatch>();
|
||||||
|
availableCategories.forEach((entry) => {
|
||||||
|
const existing = map.get(entry.category);
|
||||||
|
const swatch = existing ?? buildCategorySwatch(entry.category);
|
||||||
|
map.set(entry.category, swatch);
|
||||||
|
map.set(entry.id, swatch);
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}, [availableCategories]);
|
||||||
|
|
||||||
|
const filterSuggestions = useMemo(() => {
|
||||||
|
if (categoryFilter.trim().length === 0) {
|
||||||
|
return [] as CategoryRecord[];
|
||||||
|
}
|
||||||
|
return filteredResults.slice(0, 6);
|
||||||
|
}, [categoryFilter, filteredResults]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const node = listRef.current;
|
||||||
|
if (node) {
|
||||||
|
node.scrollTop = getTagEditorScrollTop();
|
||||||
|
}
|
||||||
|
}, [groupedCategories, collapsedGroups]);
|
||||||
|
|
||||||
|
const handleScroll = (event: UIEvent<HTMLDivElement>) => {
|
||||||
|
setTagEditorScrollTop(event.currentTarget.scrollTop);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleGroup = (groupName: string) => {
|
||||||
|
const next = new Set(collapsedGroups);
|
||||||
|
const moduleState = getCollapsedGroups()!;
|
||||||
|
if (next.has(groupName)) {
|
||||||
|
next.delete(groupName);
|
||||||
|
moduleState.delete(groupName);
|
||||||
|
} else {
|
||||||
|
next.add(groupName);
|
||||||
|
moduleState.add(groupName);
|
||||||
|
}
|
||||||
|
setModuleCollapsedGroups(moduleState);
|
||||||
|
console.log('TagEditor toggleGroup:', groupName, 'collapsed:', next.has(groupName), 'module state:', Array.from(moduleState));
|
||||||
|
setCollapsedGroups(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleCategory = (categoryId: string) => {
|
||||||
|
const next = new Set(selectedCategories);
|
||||||
|
if (next.has(categoryId)) {
|
||||||
|
next.delete(categoryId);
|
||||||
|
} else {
|
||||||
|
next.add(categoryId);
|
||||||
|
}
|
||||||
|
setSelectedCategories(next);
|
||||||
|
|
||||||
|
const parsedTags = tagDraft
|
||||||
|
.split(',')
|
||||||
|
.map((value: string) => value.trim())
|
||||||
|
.filter((value: string) => value.length > 0);
|
||||||
|
onSave({ tags: parsedTags, categories: Array.from(next) });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTagBlur = () => {
|
||||||
|
const parsedTags = tagDraft
|
||||||
|
.split(',')
|
||||||
|
.map((value: string) => value.trim())
|
||||||
|
.filter((value: string) => value.length > 0);
|
||||||
|
onSave({ tags: parsedTags, categories: Array.from(selectedCategories) });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveCategory = (categoryId: string) => {
|
||||||
|
if (!selectedCategories.has(categoryId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
toggleCategory(categoryId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearCategories = () => {
|
||||||
|
if (selectedCategories.size === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedCategories(new Set());
|
||||||
|
const parsedTags = tagDraft
|
||||||
|
.split(',')
|
||||||
|
.map((value: string) => value.trim())
|
||||||
|
.filter((value: string) => value.length > 0);
|
||||||
|
onSave({ tags: parsedTags, categories: [] });
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleCategoryList = () => {
|
||||||
|
setIsCategoryListExpanded((value) => !value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="tag-editor">
|
||||||
|
{showHeading && <h2>Tags</h2>}
|
||||||
|
<textarea
|
||||||
|
value={tagDraft}
|
||||||
|
onChange={(event: ChangeEvent<HTMLTextAreaElement>) => setTagDraft(event.target.value)}
|
||||||
|
onBlur={handleTagBlur}
|
||||||
|
placeholder="Comma separated tags"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
<div className="category-filter">
|
||||||
|
<input
|
||||||
|
value={categoryFilter}
|
||||||
|
onChange={(event: ChangeEvent<HTMLInputElement>) => setCategoryFilter(event.target.value)}
|
||||||
|
placeholder="Filter UCS categories"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{categoryFilter.trim().length > 0 && (
|
||||||
|
filterSuggestions.length > 0 ? (
|
||||||
|
<div className="category-filter-suggestions">
|
||||||
|
<span className="category-filter-suggestions-label">Suggestions</span>
|
||||||
|
<div className="category-filter-suggestion-list">
|
||||||
|
{filterSuggestions.map((entry) => {
|
||||||
|
const selected = selectedCategories.has(entry.id);
|
||||||
|
const colorStyle = createCategoryStyleVars(categoryColorMap.get(entry.id));
|
||||||
|
const categoryLabel = formatCategoryLabel(entry.category);
|
||||||
|
const subCategoryLabel = formatCategoryLabel(entry.subCategory);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={entry.id}
|
||||||
|
type="button"
|
||||||
|
className={selected ? 'category-filter-suggestion category-filter-suggestion--selected' : 'category-filter-suggestion'}
|
||||||
|
onClick={() => toggleCategory(entry.id)}
|
||||||
|
style={colorStyle}
|
||||||
|
title={`${categoryLabel} ${subCategoryLabel}`}
|
||||||
|
>
|
||||||
|
<span className="category-filter-suggestion-name">
|
||||||
|
<span className="category-label category-label--muted">{categoryLabel}</span>
|
||||||
|
<span className="category-label category-label--primary">{subCategoryLabel}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="category-filter-empty">No categories match this filter.</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<div className="selected-categories">
|
||||||
|
<div className="selected-category-chip-list">
|
||||||
|
{selectedCategoryRecords.length === 0 && <span className="selected-category-empty">No categories selected</span>}
|
||||||
|
{selectedCategoryRecords.map((entry) => {
|
||||||
|
const categoryLabel = formatCategoryLabel(entry.category);
|
||||||
|
const subCategoryLabel = formatCategoryLabel(entry.subCategory);
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={entry.id}
|
||||||
|
type="button"
|
||||||
|
className="selected-category-chip"
|
||||||
|
onClick={() => handleRemoveCategory(entry.id)}
|
||||||
|
style={createCategoryStyleVars(categoryColorMap.get(entry.id))}
|
||||||
|
title={`${categoryLabel} ${subCategoryLabel}`}
|
||||||
|
>
|
||||||
|
<span className="selected-category-chip-label">
|
||||||
|
<span className="category-label category-label--muted">{categoryLabel}</span>
|
||||||
|
<span className="category-label category-label--primary">{subCategoryLabel}</span>
|
||||||
|
</span>
|
||||||
|
<span className="selected-category-chip-remove" aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ghost-button selected-category-clear"
|
||||||
|
onClick={handleClearCategories}
|
||||||
|
disabled={selectedCategories.size === 0}
|
||||||
|
>
|
||||||
|
Clear All
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="category-list-pane">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="category-list-toggle"
|
||||||
|
onClick={toggleCategoryList}
|
||||||
|
aria-expanded={isCategoryListExpanded}
|
||||||
|
>
|
||||||
|
<span className="category-list-toggle-label">All Categories</span>
|
||||||
|
<span className="category-list-toggle-icon" aria-hidden="true">
|
||||||
|
{isCategoryListExpanded ? 'v' : '>'}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{isCategoryListExpanded && (
|
||||||
|
<div className="category-list" ref={listRef} onScroll={handleScroll}>
|
||||||
|
{Object.entries(groupedCategories).map(([groupName, groupCategories]) => {
|
||||||
|
const isCollapsed = collapsedGroups.has(groupName);
|
||||||
|
const displayGroupName = formatCategoryLabel(groupName);
|
||||||
|
return (
|
||||||
|
<div key={groupName} className="category-group">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="category-group-header"
|
||||||
|
onClick={() => toggleGroup(groupName)}
|
||||||
|
>
|
||||||
|
<span className="category-group-arrow">{isCollapsed ? '▶' : '▼'}</span>
|
||||||
|
<span className="category-group-title">{displayGroupName}</span>
|
||||||
|
</button>
|
||||||
|
{!isCollapsed && (
|
||||||
|
<div className="category-group-items">
|
||||||
|
{groupCategories.map((entry: CategoryRecord) => {
|
||||||
|
const selected = selectedCategories.has(entry.id);
|
||||||
|
const colorStyle = createCategoryStyleVars(categoryColorMap.get(entry.id));
|
||||||
|
const className = selected ? 'category-item category-item--selected' : 'category-item';
|
||||||
|
const categoryLabel = formatCategoryLabel(entry.category);
|
||||||
|
const subCategoryLabel = formatCategoryLabel(entry.subCategory);
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
key={entry.id}
|
||||||
|
className={className}
|
||||||
|
style={colorStyle}
|
||||||
|
title={`${categoryLabel} ${subCategoryLabel}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={selected}
|
||||||
|
onChange={() => toggleCategory(entry.id)}
|
||||||
|
/>
|
||||||
|
<span className="category-item-label">
|
||||||
|
<span className="category-label category-label--muted">{categoryLabel}</span>
|
||||||
|
<span className="category-label category-label--primary">{subCategoryLabel}</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TagEditor;
|
||||||
106
src/renderer/src/components/Waveform.tsx
Normal file
106
src/renderer/src/components/Waveform.tsx
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
export interface WaveformProps {
|
||||||
|
audioUrl: string | null;
|
||||||
|
currentTime: number;
|
||||||
|
duration: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders an audio waveform visualization that serves as the background for the progress bar.
|
||||||
|
*/
|
||||||
|
export function Waveform({ audioUrl, currentTime, duration, className = '' }: WaveformProps): JSX.Element {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||||
|
const waveformDataRef = useRef<number[] | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!audioUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadWaveform = async () => {
|
||||||
|
try {
|
||||||
|
const audioContext = new AudioContext();
|
||||||
|
const response = await fetch(audioUrl);
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
||||||
|
|
||||||
|
const rawData = audioBuffer.getChannelData(0);
|
||||||
|
const samples = 500;
|
||||||
|
const blockSize = Math.floor(rawData.length / samples);
|
||||||
|
const filteredData: number[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < samples; i++) {
|
||||||
|
const blockStart = blockSize * i;
|
||||||
|
let sum = 0;
|
||||||
|
for (let j = 0; j < blockSize; j++) {
|
||||||
|
sum += Math.abs(rawData[blockStart + j]);
|
||||||
|
}
|
||||||
|
filteredData.push(sum / blockSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
const multiplier = Math.max(...filteredData) ** -1;
|
||||||
|
waveformDataRef.current = filteredData.map((n) => n * multiplier);
|
||||||
|
drawWaveform();
|
||||||
|
|
||||||
|
await audioContext.close();
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to generate waveform', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void loadWaveform();
|
||||||
|
}, [audioUrl]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
drawWaveform();
|
||||||
|
}, [currentTime, duration]);
|
||||||
|
|
||||||
|
const drawWaveform = () => {
|
||||||
|
const canvas = canvasRef.current;
|
||||||
|
const data = waveformDataRef.current;
|
||||||
|
if (!canvas || !data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (!ctx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
canvas.width = rect.width * dpr;
|
||||||
|
canvas.height = rect.height * dpr;
|
||||||
|
ctx.scale(dpr, dpr);
|
||||||
|
|
||||||
|
const width = rect.width;
|
||||||
|
const height = rect.height;
|
||||||
|
const thumbWidth = 14;
|
||||||
|
const padding = thumbWidth / 2;
|
||||||
|
const drawableWidth = width - thumbWidth;
|
||||||
|
const barWidth = drawableWidth / data.length;
|
||||||
|
const playedProgress = duration > 0 ? currentTime / duration : 0;
|
||||||
|
|
||||||
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
|
for (let i = 0; i < data.length; i++) {
|
||||||
|
const barHeight = data[i] * height * 0.8;
|
||||||
|
const x = padding + (i * barWidth);
|
||||||
|
const y = (height - barHeight) / 2;
|
||||||
|
|
||||||
|
if (i / data.length < playedProgress) {
|
||||||
|
ctx.fillStyle = '#4a9eff';
|
||||||
|
} else {
|
||||||
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)';
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fillRect(x, y, barWidth * 0.8, barHeight);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <canvas ref={canvasRef} className={className} style={{ width: '100%', height: '100%' }} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Waveform;
|
||||||
18
src/renderer/src/hooks/useLibrarySnapshot.ts
Normal file
18
src/renderer/src/hooks/useLibrarySnapshot.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { useEffect, useSyncExternalStore } from 'react';
|
||||||
|
import { libraryStore, type LibrarySnapshot } from '../stores/LibraryStore';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React hook to consume the library store snapshot with automatic bootstrap.
|
||||||
|
*/
|
||||||
|
export function useLibrarySnapshot(): LibrarySnapshot {
|
||||||
|
useEffect(() => {
|
||||||
|
if (!libraryStore.getSnapshot().initialized) {
|
||||||
|
void libraryStore.bootstrap();
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return useSyncExternalStore(
|
||||||
|
(listener: () => void) => libraryStore.subscribe(listener),
|
||||||
|
() => libraryStore.getSnapshot()
|
||||||
|
);
|
||||||
|
}
|
||||||
25
src/renderer/src/hooks/usePlayerSnapshot.ts
Normal file
25
src/renderer/src/hooks/usePlayerSnapshot.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { useEffect, useSyncExternalStore } from 'react';
|
||||||
|
import { playerStore, type PlayerSnapshot } from '../stores/PlayerStore';
|
||||||
|
import type { AudioFileSummary } from '../../../shared/models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React hook to wire components to the player store.
|
||||||
|
*/
|
||||||
|
export function usePlayerSnapshot(): PlayerSnapshot {
|
||||||
|
useEffect(() => () => playerStore.dispose(), []);
|
||||||
|
return useSyncExternalStore(
|
||||||
|
(listener: () => void) => playerStore.subscribe(listener),
|
||||||
|
() => playerStore.getSnapshot()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience helper that loads a file into the audio player store.
|
||||||
|
*/
|
||||||
|
export function loadPlayerFile(file: AudioFileSummary | null, autoPlay = true): void {
|
||||||
|
if (!file) {
|
||||||
|
playerStore.dispose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
void playerStore.loadFile(file, autoPlay);
|
||||||
|
}
|
||||||
16
src/renderer/src/main.tsx
Normal file
16
src/renderer/src/main.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
import './styles/app.css';
|
||||||
|
|
||||||
|
const container = document.getElementById('root');
|
||||||
|
if (!container) {
|
||||||
|
throw new Error('Root container #root was not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = createRoot(container);
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
0
src/renderer/src/state/categoryUiState.ts
Normal file
0
src/renderer/src/state/categoryUiState.ts
Normal file
39
src/renderer/src/stores/CategoryUIState.ts
Normal file
39
src/renderer/src/stores/CategoryUIState.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* Shared UI state for category panels across TagEditor and MultiFileEditor.
|
||||||
|
* This allows collapsed/expanded state to persist when switching between files.
|
||||||
|
*/
|
||||||
|
|
||||||
|
let collapsedGroupsState: Set<string> | null = null;
|
||||||
|
let tagEditorScrollTop = 0;
|
||||||
|
let multiEditorScrollTop = 0;
|
||||||
|
|
||||||
|
export function getCollapsedGroups(): Set<string> | null {
|
||||||
|
return collapsedGroupsState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initializeCollapsedGroups(categories: string[]): void {
|
||||||
|
if (collapsedGroupsState === null) {
|
||||||
|
collapsedGroupsState = new Set(categories);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setCollapsedGroups(groups: Set<string>): void {
|
||||||
|
console.log('CategoryUIState: setCollapsedGroups called with', Array.from(groups));
|
||||||
|
collapsedGroupsState = groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTagEditorScrollTop(): number {
|
||||||
|
return tagEditorScrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setTagEditorScrollTop(scrollTop: number): void {
|
||||||
|
tagEditorScrollTop = scrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMultiEditorScrollTop(): number {
|
||||||
|
return multiEditorScrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setMultiEditorScrollTop(scrollTop: number): void {
|
||||||
|
multiEditorScrollTop = scrollTop;
|
||||||
|
}
|
||||||
507
src/renderer/src/stores/LibraryStore.ts
Normal file
507
src/renderer/src/stores/LibraryStore.ts
Normal file
@@ -0,0 +1,507 @@
|
|||||||
|
import type {
|
||||||
|
AppSettings,
|
||||||
|
AudioFileSummary,
|
||||||
|
CategoryRecord,
|
||||||
|
LibraryScanSummary,
|
||||||
|
TagUpdatePayload
|
||||||
|
} from '../../../shared/models';
|
||||||
|
|
||||||
|
type ChangeListener = () => void;
|
||||||
|
|
||||||
|
export const CATEGORY_FILTER_UNTAGGED = 'untagged' as const;
|
||||||
|
export type CategoryFilterValue = string | typeof CATEGORY_FILTER_UNTAGGED | null;
|
||||||
|
|
||||||
|
export interface LibrarySnapshot {
|
||||||
|
initialized: boolean;
|
||||||
|
files: AudioFileSummary[];
|
||||||
|
visibleFiles: AudioFileSummary[];
|
||||||
|
selectedFileId: number | null;
|
||||||
|
selectedFileIds: Set<number>;
|
||||||
|
focusedFile: AudioFileSummary | null;
|
||||||
|
categories: CategoryRecord[];
|
||||||
|
settings: AppSettings;
|
||||||
|
searchQuery: string;
|
||||||
|
categoryFilter: CategoryFilterValue;
|
||||||
|
lastScan: LibraryScanSummary | null;
|
||||||
|
metadataSuggestionsVersion: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralises renderer-side library state management with a subscription mechanism for React hooks.
|
||||||
|
*/
|
||||||
|
export class LibraryStore extends EventTarget {
|
||||||
|
private snapshot: LibrarySnapshot = {
|
||||||
|
initialized: false,
|
||||||
|
files: [],
|
||||||
|
visibleFiles: [],
|
||||||
|
selectedFileId: null,
|
||||||
|
selectedFileIds: new Set(),
|
||||||
|
focusedFile: null,
|
||||||
|
categories: [],
|
||||||
|
settings: { libraryPath: null },
|
||||||
|
searchQuery: '',
|
||||||
|
categoryFilter: null,
|
||||||
|
lastScan: null,
|
||||||
|
metadataSuggestionsVersion: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstraps the store by loading settings, categories, and the current file list.
|
||||||
|
*/
|
||||||
|
public async bootstrap(): Promise<void> {
|
||||||
|
const [settings, categories, files] = await Promise.all([
|
||||||
|
window.api.getSettings(),
|
||||||
|
window.api.listCategories(),
|
||||||
|
window.api.listAudioFiles()
|
||||||
|
]);
|
||||||
|
const firstFileId = files.at(0)?.id ?? null;
|
||||||
|
const firstFile = files.at(0) ?? null;
|
||||||
|
const nextVersion = this.snapshot.metadataSuggestionsVersion + 1;
|
||||||
|
this.snapshot = {
|
||||||
|
initialized: true,
|
||||||
|
files,
|
||||||
|
visibleFiles: [],
|
||||||
|
categories,
|
||||||
|
settings,
|
||||||
|
selectedFileId: firstFileId,
|
||||||
|
selectedFileIds: firstFileId !== null ? new Set([firstFileId]) : new Set(),
|
||||||
|
focusedFile: firstFile,
|
||||||
|
searchQuery: '',
|
||||||
|
categoryFilter: null,
|
||||||
|
lastScan: null,
|
||||||
|
metadataSuggestionsVersion: nextVersion
|
||||||
|
};
|
||||||
|
this.refreshVisibleFiles(this.snapshot.selectedFileId ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React friendly subscription helper.
|
||||||
|
*/
|
||||||
|
public subscribe(listener: ChangeListener): () => void {
|
||||||
|
const handler = () => listener();
|
||||||
|
this.addEventListener('change', handler as EventListener);
|
||||||
|
return () => this.removeEventListener('change', handler as EventListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Snapshot getter used by hooks.
|
||||||
|
*/
|
||||||
|
public getSnapshot(): LibrarySnapshot {
|
||||||
|
return this.snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the selected file id (single selection or multi-select toggle).
|
||||||
|
*/
|
||||||
|
public selectFile(fileId: number | null, options: { multi?: boolean; range?: boolean } = {}): void {
|
||||||
|
const previousFocused = this.snapshot.focusedFile;
|
||||||
|
if (fileId !== null && !this.snapshot.visibleFiles.some((file) => file.id === fileId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.multi) {
|
||||||
|
const newSelection = new Set(this.snapshot.selectedFileIds);
|
||||||
|
if (fileId !== null) {
|
||||||
|
if (newSelection.has(fileId)) {
|
||||||
|
newSelection.delete(fileId);
|
||||||
|
} else {
|
||||||
|
newSelection.add(fileId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.snapshot = {
|
||||||
|
...this.snapshot,
|
||||||
|
selectedFileId: fileId,
|
||||||
|
selectedFileIds: newSelection,
|
||||||
|
focusedFile:
|
||||||
|
fileId !== null
|
||||||
|
? this.snapshot.files.find((file) => file.id === fileId) ?? (previousFocused?.id === fileId ? previousFocused : null)
|
||||||
|
: previousFocused
|
||||||
|
};
|
||||||
|
} else if (options.range && fileId !== null && this.snapshot.selectedFileId !== null) {
|
||||||
|
const visibleIds = this.snapshot.visibleFiles.map((f) => f.id);
|
||||||
|
const currentIndex = visibleIds.indexOf(this.snapshot.selectedFileId);
|
||||||
|
const targetIndex = visibleIds.indexOf(fileId);
|
||||||
|
if (currentIndex !== -1 && targetIndex !== -1) {
|
||||||
|
const start = Math.min(currentIndex, targetIndex);
|
||||||
|
const end = Math.max(currentIndex, targetIndex);
|
||||||
|
const rangeIds = visibleIds.slice(start, end + 1);
|
||||||
|
this.snapshot = {
|
||||||
|
...this.snapshot,
|
||||||
|
selectedFileId: fileId,
|
||||||
|
selectedFileIds: new Set(rangeIds),
|
||||||
|
focusedFile:
|
||||||
|
this.snapshot.files.find((file) => file.id === fileId) ?? (previousFocused?.id === fileId ? previousFocused : null)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.snapshot = {
|
||||||
|
...this.snapshot,
|
||||||
|
selectedFileId: fileId,
|
||||||
|
selectedFileIds: fileId !== null ? new Set([fileId]) : new Set(),
|
||||||
|
focusedFile:
|
||||||
|
fileId !== null
|
||||||
|
? this.snapshot.files.find((file) => file.id === fileId) ?? (previousFocused?.id === fileId ? previousFocused : null)
|
||||||
|
: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this.emitChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selects every file currently visible in the library view.
|
||||||
|
*/
|
||||||
|
public selectAllVisibleFiles(): void {
|
||||||
|
if (this.snapshot.visibleFiles.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleIds = this.snapshot.visibleFiles.map((file) => file.id);
|
||||||
|
const primaryId = this.snapshot.selectedFileId !== null && visibleIds.includes(this.snapshot.selectedFileId)
|
||||||
|
? this.snapshot.selectedFileId
|
||||||
|
: visibleIds[0];
|
||||||
|
|
||||||
|
const previousFocused = this.snapshot.focusedFile;
|
||||||
|
const nextFocused = this.snapshot.files.find((file) => file.id === primaryId) ?? (previousFocused?.id === primaryId ? previousFocused : null);
|
||||||
|
|
||||||
|
this.snapshot = {
|
||||||
|
...this.snapshot,
|
||||||
|
selectedFileId: primaryId,
|
||||||
|
selectedFileIds: new Set(visibleIds),
|
||||||
|
focusedFile: nextFocused
|
||||||
|
};
|
||||||
|
|
||||||
|
this.emitChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Steps the selection forwards or backwards within the visible file list.
|
||||||
|
*/
|
||||||
|
public selectAdjacent(offset: number): void {
|
||||||
|
if (!Number.isFinite(offset) || offset === 0 || this.snapshot.visibleFiles.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleIds = this.snapshot.visibleFiles.map((file) => file.id);
|
||||||
|
const currentIndex = this.snapshot.selectedFileId !== null
|
||||||
|
? visibleIds.indexOf(this.snapshot.selectedFileId)
|
||||||
|
: -1;
|
||||||
|
|
||||||
|
const fallbackIndex = offset > 0 ? 0 : visibleIds.length - 1;
|
||||||
|
const baseIndex = currentIndex === -1 ? fallbackIndex : currentIndex + offset;
|
||||||
|
const nextIndex = Math.min(Math.max(baseIndex, 0), visibleIds.length - 1);
|
||||||
|
const nextId = visibleIds[nextIndex];
|
||||||
|
|
||||||
|
if (nextId === this.snapshot.selectedFileId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousFocused = this.snapshot.focusedFile;
|
||||||
|
this.snapshot = {
|
||||||
|
...this.snapshot,
|
||||||
|
selectedFileId: nextId,
|
||||||
|
selectedFileIds: new Set([nextId]),
|
||||||
|
focusedFile:
|
||||||
|
this.snapshot.files.find((file) => file.id === nextId) ?? (previousFocused?.id === nextId ? previousFocused : null)
|
||||||
|
};
|
||||||
|
this.emitChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a fuzzy search request.
|
||||||
|
*/
|
||||||
|
public async search(query: string): Promise<void> {
|
||||||
|
const results = query.trim().length === 0 ? await window.api.listAudioFiles() : await window.api.search(query);
|
||||||
|
this.snapshot = {
|
||||||
|
...this.snapshot,
|
||||||
|
files: results,
|
||||||
|
searchQuery: query,
|
||||||
|
selectedFileId: results.at(0)?.id ?? null,
|
||||||
|
focusedFile: this.snapshot.focusedFile
|
||||||
|
};
|
||||||
|
this.refreshVisibleFiles(this.snapshot.selectedFileId ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setCategoryFilter(filter: CategoryFilterValue): void {
|
||||||
|
if (this.snapshot.categoryFilter === filter) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { files, selectedFileId, focusedFile } = this.snapshot;
|
||||||
|
this.snapshot = {
|
||||||
|
...this.snapshot,
|
||||||
|
categoryFilter: filter
|
||||||
|
};
|
||||||
|
|
||||||
|
let visible: AudioFileSummary[];
|
||||||
|
if (filter === CATEGORY_FILTER_UNTAGGED) {
|
||||||
|
visible = files.filter((file) => file.categories.length === 0);
|
||||||
|
} else if (filter) {
|
||||||
|
visible = files.filter((file) => file.categories.includes(filter));
|
||||||
|
} else {
|
||||||
|
visible = files.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.snapshot = {
|
||||||
|
...this.snapshot,
|
||||||
|
visibleFiles: visible,
|
||||||
|
selectedFileId,
|
||||||
|
focusedFile
|
||||||
|
};
|
||||||
|
this.emitChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests a full library rescan and updates cached files accordingly.
|
||||||
|
*/
|
||||||
|
public async rescan(): Promise<LibraryScanSummary> {
|
||||||
|
const summary = await window.api.rescanLibrary();
|
||||||
|
const files = await window.api.listAudioFiles();
|
||||||
|
this.snapshot = {
|
||||||
|
...this.snapshot,
|
||||||
|
files,
|
||||||
|
selectedFileId: files.at(0)?.id ?? null,
|
||||||
|
lastScan: summary,
|
||||||
|
focusedFile: this.snapshot.focusedFile,
|
||||||
|
metadataSuggestionsVersion: this.snapshot.metadataSuggestionsVersion + 1
|
||||||
|
};
|
||||||
|
this.refreshVisibleFiles(this.snapshot.selectedFileId ?? null);
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies tag mutations to both the backend and the cached snapshot.
|
||||||
|
*/
|
||||||
|
public async updateTagging(payload: TagUpdatePayload): Promise<AudioFileSummary> {
|
||||||
|
const updated = await window.api.updateTagging(payload);
|
||||||
|
this.applyFilePatch(updated);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renames a file and replaces it inside the store.
|
||||||
|
*/
|
||||||
|
public async renameFile(fileId: number, newName: string): Promise<AudioFileSummary> {
|
||||||
|
const updated = await window.api.renameFile(fileId, newName);
|
||||||
|
this.applyFilePatch(updated);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves a file to another relative directory within the library.
|
||||||
|
*/
|
||||||
|
public async moveFile(fileId: number, targetRelativeDirectory: string): Promise<AudioFileSummary> {
|
||||||
|
const updated = await window.api.moveFile(fileId, targetRelativeDirectory);
|
||||||
|
this.applyFilePatch(updated);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Automatically organizes a file based on its categories.
|
||||||
|
*/
|
||||||
|
public async organizeFile(fileId: number, metadata: { customName?: string | null; author?: string | null; copyright?: string | null; rating?: number }): Promise<AudioFileSummary> {
|
||||||
|
const currentSelection = this.snapshot.selectedFileId;
|
||||||
|
const currentFocused = this.snapshot.focusedFile;
|
||||||
|
const updated = await window.api.organizeFile(fileId, metadata);
|
||||||
|
const files = await window.api.listAudioFiles();
|
||||||
|
|
||||||
|
const { categoryFilter } = this.snapshot;
|
||||||
|
let visible: AudioFileSummary[];
|
||||||
|
if (categoryFilter === CATEGORY_FILTER_UNTAGGED) {
|
||||||
|
visible = files.filter((file) => file.categories.length === 0);
|
||||||
|
} else if (categoryFilter) {
|
||||||
|
visible = files.filter((file) => file.categories.includes(categoryFilter));
|
||||||
|
} else {
|
||||||
|
visible = files.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.snapshot = {
|
||||||
|
...this.snapshot,
|
||||||
|
files,
|
||||||
|
visibleFiles: visible,
|
||||||
|
selectedFileId: currentSelection,
|
||||||
|
focusedFile: currentFocused,
|
||||||
|
metadataSuggestionsVersion: this.snapshot.metadataSuggestionsVersion + 1
|
||||||
|
};
|
||||||
|
this.emitChange();
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the custom name for a file without moving it.
|
||||||
|
*/
|
||||||
|
public async updateCustomName(fileId: number, customName: string | null): Promise<AudioFileSummary> {
|
||||||
|
const updated = await window.api.updateCustomName(fileId, customName);
|
||||||
|
this.applyFilePatch(updated);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates metadata (author, copyright, rating) for a file without organizing it.
|
||||||
|
*/
|
||||||
|
public async updateFileMetadata(fileId: number, metadata: { author?: string | null; copyright?: string | null; rating?: number }): Promise<void> {
|
||||||
|
await window.api.updateFileMetadata(fileId, metadata);
|
||||||
|
|
||||||
|
// Refresh the file list to get updated metadata
|
||||||
|
const files = await window.api.listAudioFiles();
|
||||||
|
const { categoryFilter } = this.snapshot;
|
||||||
|
let visible: AudioFileSummary[];
|
||||||
|
if (categoryFilter === CATEGORY_FILTER_UNTAGGED) {
|
||||||
|
visible = files.filter((file) => file.categories.length === 0);
|
||||||
|
} else if (categoryFilter) {
|
||||||
|
visible = files.filter((file) => file.categories.includes(categoryFilter));
|
||||||
|
} else {
|
||||||
|
visible = files.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.snapshot = {
|
||||||
|
...this.snapshot,
|
||||||
|
files,
|
||||||
|
visibleFiles: visible,
|
||||||
|
metadataSuggestionsVersion: this.snapshot.metadataSuggestionsVersion + 1
|
||||||
|
};
|
||||||
|
this.emitChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assigns a new library path and refreshes the cached settings + file list.
|
||||||
|
*/
|
||||||
|
public async setLibraryPath(targetPath: string): Promise<void> {
|
||||||
|
const settings = await window.api.setLibraryPath(targetPath);
|
||||||
|
const files = await window.api.listAudioFiles();
|
||||||
|
this.snapshot = {
|
||||||
|
...this.snapshot,
|
||||||
|
settings,
|
||||||
|
files,
|
||||||
|
selectedFileId: files.at(0)?.id ?? null,
|
||||||
|
categoryFilter: null,
|
||||||
|
focusedFile: this.snapshot.focusedFile,
|
||||||
|
metadataSuggestionsVersion: this.snapshot.metadataSuggestionsVersion + 1
|
||||||
|
};
|
||||||
|
this.refreshVisibleFiles(this.snapshot.selectedFileId ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the currently selected file object.
|
||||||
|
*/
|
||||||
|
public getSelectedFile(): AudioFileSummary | null {
|
||||||
|
if (!this.snapshot.selectedFileId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.snapshot.files.find((file) => file.id === this.snapshot.selectedFileId) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience for retrieving the current settings.
|
||||||
|
*/
|
||||||
|
public getSettings(): AppSettings {
|
||||||
|
return this.snapshot.settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits a change event for subscribers.
|
||||||
|
*/
|
||||||
|
private emitChange(): void {
|
||||||
|
this.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private refreshVisibleFiles(preferredId?: number | null, emit = true): void {
|
||||||
|
const { files, categoryFilter } = this.snapshot;
|
||||||
|
let visible: AudioFileSummary[];
|
||||||
|
if (categoryFilter === CATEGORY_FILTER_UNTAGGED) {
|
||||||
|
visible = files.filter((file) => file.categories.length === 0);
|
||||||
|
} else if (categoryFilter) {
|
||||||
|
visible = files.filter((file) => file.categories.includes(categoryFilter));
|
||||||
|
} else {
|
||||||
|
visible = files.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
const desiredId = preferredId !== undefined ? preferredId : this.snapshot.selectedFileId;
|
||||||
|
const enforceVisible = preferredId !== undefined;
|
||||||
|
const nextSelected = this.resolveSelectedId(visible, desiredId, enforceVisible);
|
||||||
|
|
||||||
|
const nextSelectionSet = new Set<number>();
|
||||||
|
if (enforceVisible) {
|
||||||
|
const visibleIds = new Set(visible.map((file) => file.id));
|
||||||
|
for (const id of this.snapshot.selectedFileIds) {
|
||||||
|
if (visibleIds.has(id)) {
|
||||||
|
nextSelectionSet.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const id of this.snapshot.selectedFileIds) {
|
||||||
|
nextSelectionSet.add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nextSelected !== null) {
|
||||||
|
nextSelectionSet.add(nextSelected);
|
||||||
|
}
|
||||||
|
const previousFocused = this.snapshot.focusedFile;
|
||||||
|
const nextFocused = nextSelected !== null
|
||||||
|
? this.snapshot.files.find((file) => file.id === nextSelected) ?? (previousFocused?.id === nextSelected ? previousFocused : null)
|
||||||
|
: null;
|
||||||
|
this.snapshot = {
|
||||||
|
...this.snapshot,
|
||||||
|
visibleFiles: visible,
|
||||||
|
selectedFileId: nextSelected,
|
||||||
|
selectedFileIds: nextSelectionSet,
|
||||||
|
focusedFile: nextFocused
|
||||||
|
};
|
||||||
|
|
||||||
|
if (emit) {
|
||||||
|
this.emitChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveSelectedId(visible: AudioFileSummary[], desiredId: number | null, enforceVisible: boolean): number | null {
|
||||||
|
if (desiredId !== null) {
|
||||||
|
if (!enforceVisible) {
|
||||||
|
return desiredId;
|
||||||
|
}
|
||||||
|
if (visible.some((file) => file.id === desiredId)) {
|
||||||
|
return desiredId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (enforceVisible) {
|
||||||
|
return visible.at(0)?.id ?? null;
|
||||||
|
}
|
||||||
|
return desiredId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces a single file entry within the cached collection.
|
||||||
|
*/
|
||||||
|
private applyFilePatch(updated: AudioFileSummary): void {
|
||||||
|
const nextFiles = this.snapshot.files.slice();
|
||||||
|
const index = nextFiles.findIndex((file) => file.id === updated.id);
|
||||||
|
const previous = index >= 0 ? nextFiles[index] : null;
|
||||||
|
if (index >= 0) {
|
||||||
|
nextFiles[index] = updated;
|
||||||
|
} else {
|
||||||
|
nextFiles.push(updated);
|
||||||
|
}
|
||||||
|
const shouldBumpSuggestions = previous !== null && (previous.fileName !== updated.fileName || previous.relativePath !== updated.relativePath);
|
||||||
|
|
||||||
|
const { categoryFilter, selectedFileId, focusedFile } = this.snapshot;
|
||||||
|
let visible: AudioFileSummary[];
|
||||||
|
if (categoryFilter === CATEGORY_FILTER_UNTAGGED) {
|
||||||
|
visible = nextFiles.filter((file) => file.categories.length === 0);
|
||||||
|
} else if (categoryFilter) {
|
||||||
|
visible = nextFiles.filter((file) => file.categories.includes(categoryFilter));
|
||||||
|
} else {
|
||||||
|
visible = nextFiles.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.snapshot = {
|
||||||
|
...this.snapshot,
|
||||||
|
files: nextFiles,
|
||||||
|
visibleFiles: visible,
|
||||||
|
selectedFileId,
|
||||||
|
focusedFile,
|
||||||
|
metadataSuggestionsVersion: shouldBumpSuggestions
|
||||||
|
? this.snapshot.metadataSuggestionsVersion + 1
|
||||||
|
: this.snapshot.metadataSuggestionsVersion
|
||||||
|
};
|
||||||
|
this.emitChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const libraryStore = new LibraryStore();
|
||||||
78
src/renderer/src/stores/PlayerStore.ts
Normal file
78
src/renderer/src/stores/PlayerStore.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import type { AudioFileSummary } from '../../../shared/models';
|
||||||
|
|
||||||
|
type PlayerListener = () => void;
|
||||||
|
|
||||||
|
export interface PlayerSnapshot {
|
||||||
|
status: 'idle' | 'loading' | 'ready' | 'error';
|
||||||
|
currentFileId: number | null;
|
||||||
|
audioUrl: string | null;
|
||||||
|
autoPlay: boolean;
|
||||||
|
error: string | null;
|
||||||
|
loadCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal audio playback store that fetches WAV data through the preload bridge and exposes a blob URL.
|
||||||
|
*/
|
||||||
|
export class PlayerStore extends EventTarget {
|
||||||
|
private snapshot: PlayerSnapshot = {
|
||||||
|
status: 'idle',
|
||||||
|
currentFileId: null,
|
||||||
|
audioUrl: null,
|
||||||
|
autoPlay: false,
|
||||||
|
error: null,
|
||||||
|
loadCount: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the latest snapshot.
|
||||||
|
*/
|
||||||
|
public getSnapshot(): PlayerSnapshot {
|
||||||
|
return this.snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes to updates.
|
||||||
|
*/
|
||||||
|
public subscribe(listener: PlayerListener): () => void {
|
||||||
|
const handler = () => listener();
|
||||||
|
this.addEventListener('change', handler as EventListener);
|
||||||
|
return () => this.removeEventListener('change', handler as EventListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads a new file from disk, emitting progress updates.
|
||||||
|
*/
|
||||||
|
public async loadFile(file: AudioFileSummary, autoPlay = true): Promise<void> {
|
||||||
|
this.update({ status: 'loading', currentFileId: file.id, autoPlay, error: null });
|
||||||
|
try {
|
||||||
|
const payload = await window.api.getAudioBuffer(file.id);
|
||||||
|
const blob = new Blob([payload.buffer], { type: payload.mimeType });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
if (this.snapshot.audioUrl) {
|
||||||
|
URL.revokeObjectURL(this.snapshot.audioUrl);
|
||||||
|
}
|
||||||
|
this.update({ status: 'ready', audioUrl: url, autoPlay, loadCount: this.snapshot.loadCount + 1 });
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Failed to load audio file.';
|
||||||
|
this.update({ status: 'error', error: message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Releases resources when the player is disposed.
|
||||||
|
*/
|
||||||
|
public dispose(): void {
|
||||||
|
if (this.snapshot.audioUrl) {
|
||||||
|
URL.revokeObjectURL(this.snapshot.audioUrl);
|
||||||
|
}
|
||||||
|
this.update({ status: 'idle', currentFileId: null, audioUrl: null, autoPlay: false, error: null, loadCount: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
private update(patch: Partial<PlayerSnapshot>): void {
|
||||||
|
this.snapshot = { ...this.snapshot, ...patch };
|
||||||
|
this.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const playerStore = new PlayerStore();
|
||||||
1566
src/renderer/src/styles/app.css
Normal file
1566
src/renderer/src/styles/app.css
Normal file
File diff suppressed because it is too large
Load Diff
66
src/renderer/src/utils/categoryColors.ts
Normal file
66
src/renderer/src/utils/categoryColors.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import type { CSSProperties } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared helpers for deriving deterministic color accents for UCS categories.
|
||||||
|
*/
|
||||||
|
export interface CategorySwatch {
|
||||||
|
base: string;
|
||||||
|
soft: string;
|
||||||
|
strong: string;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a deterministic numeric hash for a category label.
|
||||||
|
*/
|
||||||
|
export function hashCategoryLabel(label: string): number {
|
||||||
|
let hash = 0;
|
||||||
|
for (let index = 0; index < label.length; index += 1) {
|
||||||
|
hash = (hash * 31 + label.charCodeAt(index)) >>> 0;
|
||||||
|
}
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a hue-based color swatch derived from the provided category label.
|
||||||
|
*/
|
||||||
|
export function buildCategorySwatch(label: string): CategorySwatch {
|
||||||
|
const normalized = label.trim().toLowerCase();
|
||||||
|
const hue = hashCategoryLabel(normalized) % 360;
|
||||||
|
const saturation = 62;
|
||||||
|
const lightness = 52;
|
||||||
|
|
||||||
|
const base = `hsl(${hue}, ${saturation}%, ${lightness}%)`;
|
||||||
|
const soft = `hsla(${hue}, ${saturation}%, ${lightness}%, 0.18)`;
|
||||||
|
const strong = `hsla(${hue}, ${saturation}%, ${Math.min(lightness + 8, 70)}%, 0.35)`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
base,
|
||||||
|
soft,
|
||||||
|
strong,
|
||||||
|
text: '#ffffff'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates CSS custom property assignments from a swatch.
|
||||||
|
*/
|
||||||
|
export function createCategoryStyleVars(swatch?: CategorySwatch): CSSProperties | undefined {
|
||||||
|
if (!swatch) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
'--category-color-base': swatch.base,
|
||||||
|
'--category-color-soft': swatch.soft,
|
||||||
|
'--category-color-strong': swatch.strong,
|
||||||
|
'--category-color-text': swatch.text
|
||||||
|
} as CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats UCS labels from uppercase source data into readable casing.
|
||||||
|
*/
|
||||||
|
export function formatCategoryLabel(value: string): string {
|
||||||
|
const lower = value.trim().toLowerCase();
|
||||||
|
return lower.replace(/(^|[\s\-\/'])([a-z])/g, (match, boundary, letter) => `${boundary}${letter.toUpperCase()}`);
|
||||||
|
}
|
||||||
80
src/shared/ipc.ts
Normal file
80
src/shared/ipc.ts
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
/** Enumerates IPC channels shared between renderer and main. */
|
||||||
|
export const IPC_CHANNELS = {
|
||||||
|
settingsGet: 'settings:get',
|
||||||
|
settingsSetLibrary: 'settings:set-library',
|
||||||
|
dialogSelectLibrary: 'dialog:select-library',
|
||||||
|
libraryScan: 'library:scan',
|
||||||
|
libraryList: 'library:list',
|
||||||
|
libraryDuplicates: 'library:duplicates',
|
||||||
|
libraryRename: 'library:rename',
|
||||||
|
libraryMove: 'library:move',
|
||||||
|
libraryOrganize: 'library:organize',
|
||||||
|
libraryCustomName: 'library:custom-name',
|
||||||
|
libraryOpenFolder: 'library:open-folder',
|
||||||
|
libraryDelete: 'library:delete',
|
||||||
|
libraryBuffer: 'library:buffer',
|
||||||
|
libraryMetadata: 'library:metadata',
|
||||||
|
libraryMetadataSuggestions: 'library:metadata-suggestions',
|
||||||
|
libraryUpdateMetadata: 'library:update-metadata',
|
||||||
|
libraryWaveformPreview: 'library:waveform-preview',
|
||||||
|
tagsUpdate: 'tags:update',
|
||||||
|
categoriesList: 'categories:list',
|
||||||
|
searchQuery: 'search:query'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type IpcChannelKeys = keyof typeof IPC_CHANNELS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API surface exposed to the renderer through the preload script.
|
||||||
|
*/
|
||||||
|
export interface RendererApi {
|
||||||
|
/** Retrieves the persisted settings object. */
|
||||||
|
getSettings(): Promise<import('./models').AppSettings>;
|
||||||
|
/** Opens a dialog to select a new library path, returning the chosen directory or null. */
|
||||||
|
selectLibraryDirectory(): Promise<string | null>;
|
||||||
|
/** Persists the given library path. */
|
||||||
|
setLibraryPath(path: string): Promise<import('./models').AppSettings>;
|
||||||
|
/** Triggers a manual rescan of the library. */
|
||||||
|
rescanLibrary(): Promise<import('./models').LibraryScanSummary>;
|
||||||
|
/** Fetches the current list of audio files. */
|
||||||
|
listAudioFiles(): Promise<import('./models').AudioFileSummary[]>;
|
||||||
|
/** Fetches groups of duplicate files based on checksum. */
|
||||||
|
listDuplicates(): Promise<{ checksum: string; files: import('./models').AudioFileSummary[] }[]>;
|
||||||
|
/** Requests a rename for the target file. */
|
||||||
|
renameFile(fileId: number, newFileName: string): Promise<import('./models').AudioFileSummary>;
|
||||||
|
/** Moves a file to another subdirectory under the library root. */
|
||||||
|
moveFile(fileId: number, targetRelativeDirectory: string): Promise<import('./models').AudioFileSummary>;
|
||||||
|
/** Automatically organizes a file based on its categories. */
|
||||||
|
organizeFile(fileId: number, metadata: { customName?: string | null; author?: string | null; copyright?: string | null; rating?: number }): Promise<import('./models').AudioFileSummary>;
|
||||||
|
/** Updates the custom name for a file. */
|
||||||
|
updateCustomName(fileId: number, customName: string | null): Promise<import('./models').AudioFileSummary>;
|
||||||
|
/** Opens the file's containing folder in the system file explorer. */
|
||||||
|
openFileFolder(fileId: number): Promise<void>;
|
||||||
|
/** Deletes files from disk and database. */
|
||||||
|
deleteFiles(fileIds: number[]): Promise<void>;
|
||||||
|
/** Fetches the binary data required for playback. */
|
||||||
|
getAudioBuffer(fileId: number): Promise<import('./models').AudioBufferPayload>;
|
||||||
|
/** Returns a lightweight waveform preview for rendering list backgrounds. */
|
||||||
|
getWaveformPreview(fileId: number, pointCount?: number): Promise<{ samples: number[]; rms: number }>;
|
||||||
|
/** Updates free-form tags and UCS categories. */
|
||||||
|
updateTagging(payload: import('./models').TagUpdatePayload): Promise<import('./models').AudioFileSummary>;
|
||||||
|
/** Returns the catalog of UCS categories. */
|
||||||
|
listCategories(): Promise<import('./models').CategoryRecord[]>;
|
||||||
|
/** Runs fuzzy search returning ranked results. */
|
||||||
|
search(query: string): Promise<import('./models').AudioFileSummary[]>;
|
||||||
|
/** Reads embedded metadata from a WAV file. */
|
||||||
|
readFileMetadata(fileId: number): Promise<{ author?: string; copyright?: string; title?: string; rating?: number }>;
|
||||||
|
/** Lists distinct metadata values gathered from the library for quick suggestions. */
|
||||||
|
listMetadataSuggestions(): Promise<{ authors: string[]; copyrights: string[] }>;
|
||||||
|
/** Updates metadata (author, copyright, rating) without organizing the file. */
|
||||||
|
updateFileMetadata(fileId: number, metadata: { author?: string | null; copyright?: string | null; rating?: number }): Promise<void>;
|
||||||
|
/** Listens for menu actions from the main process. Returns a cleanup function. */
|
||||||
|
onMenuAction(channel: string, callback: () => void): () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
/** Bridge API injected by the preload script. */
|
||||||
|
api: RendererApi;
|
||||||
|
}
|
||||||
|
}
|
||||||
109
src/shared/models.ts
Normal file
109
src/shared/models.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
/**
|
||||||
|
* Represents a summarized view of an audio file for list displays and searches.
|
||||||
|
*/
|
||||||
|
export interface AudioFileSummary {
|
||||||
|
/** Unique identifier inside the database. */
|
||||||
|
id: number;
|
||||||
|
/** Filename with extension. */
|
||||||
|
fileName: string;
|
||||||
|
/** User-friendly display name (no extension). */
|
||||||
|
displayName: string;
|
||||||
|
/** Absolute path on disk. */
|
||||||
|
absolutePath: string;
|
||||||
|
/** Path relative to the configured library root. */
|
||||||
|
relativePath: string;
|
||||||
|
/** Last modified timestamp (epoch milliseconds). */
|
||||||
|
modifiedAt: number;
|
||||||
|
/** File creation timestamp (epoch milliseconds) if available. */
|
||||||
|
createdAt: number | null;
|
||||||
|
/** File size in bytes. */
|
||||||
|
size: number;
|
||||||
|
/** Duration in milliseconds if known. */
|
||||||
|
durationMs: number | null;
|
||||||
|
/** Sample rate in Hz if known. */
|
||||||
|
sampleRate: number | null;
|
||||||
|
/** Bit depth if known. */
|
||||||
|
bitDepth: number | null;
|
||||||
|
/** Content checksum used for duplicate detection. */
|
||||||
|
checksum: string | null;
|
||||||
|
/** Applied descriptive tags. */
|
||||||
|
tags: string[];
|
||||||
|
/** Associated UCS categories. */
|
||||||
|
categories: string[];
|
||||||
|
/** Custom name for file organization. */
|
||||||
|
customName: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a group of files that share the same checksum.
|
||||||
|
*/
|
||||||
|
export interface DuplicateGroup {
|
||||||
|
/** MD5 checksum shared by all files in the group. */
|
||||||
|
checksum: string;
|
||||||
|
/** Files that have identical content. */
|
||||||
|
files: AudioFileSummary[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Structure describing a UCS category.
|
||||||
|
*/
|
||||||
|
export interface CategoryRecord {
|
||||||
|
/** Category identifier (CatID from UCS). */
|
||||||
|
id: string;
|
||||||
|
/** High level category name. */
|
||||||
|
category: string;
|
||||||
|
/** Sub-category label. */
|
||||||
|
subCategory: string;
|
||||||
|
/** Short hand for the category. */
|
||||||
|
shortCode: string;
|
||||||
|
/** Human readable explanation. */
|
||||||
|
explanation: string;
|
||||||
|
/** List of synonym keywords. */
|
||||||
|
synonyms: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Summary report for a library scan run.
|
||||||
|
*/
|
||||||
|
export interface LibraryScanSummary {
|
||||||
|
/** Number of newly discovered files. */
|
||||||
|
added: number;
|
||||||
|
/** Number of existing records refreshed. */
|
||||||
|
updated: number;
|
||||||
|
/** Number of database records removed because the files disappeared. */
|
||||||
|
removed: number;
|
||||||
|
/** Total records after the scan. */
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shape of persisted application settings.
|
||||||
|
*/
|
||||||
|
export interface AppSettings {
|
||||||
|
/** Current library root path. */
|
||||||
|
libraryPath: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payload for tag updates flowing from the renderer to the main process.
|
||||||
|
*/
|
||||||
|
export interface TagUpdatePayload {
|
||||||
|
/** Target audio file id. */
|
||||||
|
fileId: number;
|
||||||
|
/** Free-form tag collection. */
|
||||||
|
tags: string[];
|
||||||
|
/** UCS category identifiers to attach. */
|
||||||
|
categories: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response for playback requests containing file binary data.
|
||||||
|
*/
|
||||||
|
export interface AudioBufferPayload {
|
||||||
|
/** Original file id to trace usage. */
|
||||||
|
fileId: number;
|
||||||
|
/** Raw audio data. */
|
||||||
|
buffer: ArrayBuffer;
|
||||||
|
/** MIME type for the audio data. */
|
||||||
|
mimeType: string;
|
||||||
|
}
|
||||||
433
src/test/DatabaseService.test.ts
Normal file
433
src/test/DatabaseService.test.ts
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
/**
|
||||||
|
* Comprehensive test suite for DatabaseService.
|
||||||
|
* Run with: node --import tsx --test src/test/DatabaseService.test.ts
|
||||||
|
*/
|
||||||
|
import { describe, it, beforeEach, afterEach } from 'node:test';
|
||||||
|
import assert from 'node:assert';
|
||||||
|
import { DatabaseService } from '../main/services/DatabaseService';
|
||||||
|
import { getTempDbPath } from './testHelpers';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
|
||||||
|
describe('DatabaseService', () => {
|
||||||
|
let database: DatabaseService;
|
||||||
|
let dbPath: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
dbPath = getTempDbPath();
|
||||||
|
database = new DatabaseService(dbPath);
|
||||||
|
database.initialize();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
database.close();
|
||||||
|
await fs.unlink(dbPath).catch(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('file management', () => {
|
||||||
|
it('should upsert file record', () => {
|
||||||
|
const record = database.upsertFile({
|
||||||
|
absolutePath: '/test/file.wav',
|
||||||
|
relativePath: 'file.wav',
|
||||||
|
fileName: 'file.wav',
|
||||||
|
displayName: 'file',
|
||||||
|
modifiedAt: Date.now(),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
size: 1024,
|
||||||
|
durationMs: 1000,
|
||||||
|
sampleRate: 44100,
|
||||||
|
bitDepth: 16,
|
||||||
|
checksum: 'abc123',
|
||||||
|
tags: ['test'],
|
||||||
|
categories: []
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.ok(record.id);
|
||||||
|
assert.strictEqual(record.fileName, 'file.wav');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should list all files', () => {
|
||||||
|
database.upsertFile({
|
||||||
|
absolutePath: '/test/file1.wav',
|
||||||
|
relativePath: 'file1.wav',
|
||||||
|
fileName: 'file1.wav',
|
||||||
|
displayName: 'file1',
|
||||||
|
modifiedAt: Date.now(),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
size: 1024,
|
||||||
|
durationMs: 1000,
|
||||||
|
sampleRate: 44100,
|
||||||
|
bitDepth: 16,
|
||||||
|
checksum: null,
|
||||||
|
tags: [],
|
||||||
|
categories: []
|
||||||
|
});
|
||||||
|
|
||||||
|
database.upsertFile({
|
||||||
|
absolutePath: '/test/file2.wav',
|
||||||
|
relativePath: 'file2.wav',
|
||||||
|
fileName: 'file2.wav',
|
||||||
|
displayName: 'file2',
|
||||||
|
modifiedAt: Date.now(),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
size: 1024,
|
||||||
|
durationMs: 1000,
|
||||||
|
sampleRate: 44100,
|
||||||
|
bitDepth: 16,
|
||||||
|
checksum: null,
|
||||||
|
tags: [],
|
||||||
|
categories: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const files = database.listFiles();
|
||||||
|
assert.strictEqual(files.length, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get file by id', () => {
|
||||||
|
const inserted = database.upsertFile({
|
||||||
|
absolutePath: '/test/file.wav',
|
||||||
|
relativePath: 'file.wav',
|
||||||
|
fileName: 'file.wav',
|
||||||
|
displayName: 'file',
|
||||||
|
modifiedAt: Date.now(),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
size: 1024,
|
||||||
|
durationMs: 1000,
|
||||||
|
sampleRate: 44100,
|
||||||
|
bitDepth: 16,
|
||||||
|
checksum: null,
|
||||||
|
tags: [],
|
||||||
|
categories: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const retrieved = database.getFileById(inserted.id);
|
||||||
|
assert.strictEqual(retrieved.id, inserted.id);
|
||||||
|
assert.strictEqual(retrieved.fileName, 'file.wav');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update file location', () => {
|
||||||
|
const file = database.upsertFile({
|
||||||
|
absolutePath: '/test/file.wav',
|
||||||
|
relativePath: 'file.wav',
|
||||||
|
fileName: 'file.wav',
|
||||||
|
displayName: 'file',
|
||||||
|
modifiedAt: Date.now(),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
size: 1024,
|
||||||
|
durationMs: 1000,
|
||||||
|
sampleRate: 44100,
|
||||||
|
bitDepth: 16,
|
||||||
|
checksum: null,
|
||||||
|
tags: [],
|
||||||
|
categories: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const updated = database.updateFileLocation(
|
||||||
|
file.id,
|
||||||
|
'/test/newdir/file.wav',
|
||||||
|
'newdir/file.wav',
|
||||||
|
'file.wav',
|
||||||
|
'file'
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(updated.absolutePath, '/test/newdir/file.wav');
|
||||||
|
assert.strictEqual(updated.relativePath, 'newdir/file.wav');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete file', () => {
|
||||||
|
const file = database.upsertFile({
|
||||||
|
absolutePath: '/test/file.wav',
|
||||||
|
relativePath: 'file.wav',
|
||||||
|
fileName: 'file.wav',
|
||||||
|
displayName: 'file',
|
||||||
|
modifiedAt: Date.now(),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
size: 1024,
|
||||||
|
durationMs: 1000,
|
||||||
|
sampleRate: 44100,
|
||||||
|
bitDepth: 16,
|
||||||
|
checksum: null,
|
||||||
|
tags: [],
|
||||||
|
categories: []
|
||||||
|
});
|
||||||
|
|
||||||
|
database.deleteFile(file.id);
|
||||||
|
|
||||||
|
const files = database.listFiles();
|
||||||
|
assert.strictEqual(files.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove files outside set', () => {
|
||||||
|
database.upsertFile({
|
||||||
|
absolutePath: '/test/file1.wav',
|
||||||
|
relativePath: 'file1.wav',
|
||||||
|
fileName: 'file1.wav',
|
||||||
|
displayName: 'file1',
|
||||||
|
modifiedAt: Date.now(),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
size: 1024,
|
||||||
|
durationMs: 1000,
|
||||||
|
sampleRate: 44100,
|
||||||
|
bitDepth: 16,
|
||||||
|
checksum: null,
|
||||||
|
tags: [],
|
||||||
|
categories: []
|
||||||
|
});
|
||||||
|
|
||||||
|
database.upsertFile({
|
||||||
|
absolutePath: '/test/file2.wav',
|
||||||
|
relativePath: 'file2.wav',
|
||||||
|
fileName: 'file2.wav',
|
||||||
|
displayName: 'file2',
|
||||||
|
modifiedAt: Date.now(),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
size: 1024,
|
||||||
|
durationMs: 1000,
|
||||||
|
sampleRate: 44100,
|
||||||
|
bitDepth: 16,
|
||||||
|
checksum: null,
|
||||||
|
tags: [],
|
||||||
|
categories: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const kept = new Set(['/test/file1.wav']);
|
||||||
|
const removed = database.removeFilesOutside(kept);
|
||||||
|
|
||||||
|
assert.strictEqual(removed, 1);
|
||||||
|
assert.strictEqual(database.listFiles().length, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('tagging', () => {
|
||||||
|
it('should update file tags', () => {
|
||||||
|
const file = database.upsertFile({
|
||||||
|
absolutePath: '/test/file.wav',
|
||||||
|
relativePath: 'file.wav',
|
||||||
|
fileName: 'file.wav',
|
||||||
|
displayName: 'file',
|
||||||
|
modifiedAt: Date.now(),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
size: 1024,
|
||||||
|
durationMs: 1000,
|
||||||
|
sampleRate: 44100,
|
||||||
|
bitDepth: 16,
|
||||||
|
checksum: null,
|
||||||
|
tags: [],
|
||||||
|
categories: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const updated = database.updateTagging(file.id, ['tag1', 'tag2'], ['cat1']);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(updated.tags, ['tag1', 'tag2']);
|
||||||
|
assert.deepStrictEqual(updated.categories, ['cat1']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update custom name', () => {
|
||||||
|
const file = database.upsertFile({
|
||||||
|
absolutePath: '/test/file.wav',
|
||||||
|
relativePath: 'file.wav',
|
||||||
|
fileName: 'file.wav',
|
||||||
|
displayName: 'file',
|
||||||
|
modifiedAt: Date.now(),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
size: 1024,
|
||||||
|
durationMs: 1000,
|
||||||
|
sampleRate: 44100,
|
||||||
|
bitDepth: 16,
|
||||||
|
checksum: null,
|
||||||
|
tags: [],
|
||||||
|
categories: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const updated = database.updateCustomName(file.id, 'Custom Name');
|
||||||
|
|
||||||
|
assert.strictEqual(updated.customName, 'Custom Name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear custom name', () => {
|
||||||
|
const file = database.upsertFile({
|
||||||
|
absolutePath: '/test/file.wav',
|
||||||
|
relativePath: 'file.wav',
|
||||||
|
fileName: 'file.wav',
|
||||||
|
displayName: 'file',
|
||||||
|
modifiedAt: Date.now(),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
size: 1024,
|
||||||
|
durationMs: 1000,
|
||||||
|
sampleRate: 44100,
|
||||||
|
bitDepth: 16,
|
||||||
|
checksum: null,
|
||||||
|
tags: [],
|
||||||
|
categories: []
|
||||||
|
});
|
||||||
|
|
||||||
|
database.updateCustomName(file.id, 'Custom Name');
|
||||||
|
const updated = database.updateCustomName(file.id, null);
|
||||||
|
|
||||||
|
assert.strictEqual(updated.customName, null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('categories', () => {
|
||||||
|
it('should upsert category', () => {
|
||||||
|
database.upsertCategory({
|
||||||
|
id: 'TEST',
|
||||||
|
category: 'TEST CATEGORY',
|
||||||
|
subCategory: 'Test Sub',
|
||||||
|
shortCode: 'TST',
|
||||||
|
explanation: 'Test explanation',
|
||||||
|
synonyms: ['test', 'sample']
|
||||||
|
});
|
||||||
|
|
||||||
|
const categories = database.listCategories();
|
||||||
|
assert.ok(categories.some(c => c.id === 'TEST'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get category by id', () => {
|
||||||
|
database.upsertCategory({
|
||||||
|
id: 'TEST',
|
||||||
|
category: 'TEST CATEGORY',
|
||||||
|
subCategory: 'Test Sub',
|
||||||
|
shortCode: 'TST',
|
||||||
|
explanation: 'Test explanation',
|
||||||
|
synonyms: ['test', 'sample']
|
||||||
|
});
|
||||||
|
|
||||||
|
const category = database.getCategoryById('TEST');
|
||||||
|
assert.ok(category);
|
||||||
|
assert.strictEqual(category.id, 'TEST');
|
||||||
|
assert.strictEqual(category.shortCode, 'TST');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should list all categories', () => {
|
||||||
|
database.upsertCategory({
|
||||||
|
id: 'TEST1',
|
||||||
|
category: 'TEST CATEGORY',
|
||||||
|
subCategory: 'Test Sub 1',
|
||||||
|
shortCode: 'TS1',
|
||||||
|
explanation: 'Test',
|
||||||
|
synonyms: []
|
||||||
|
});
|
||||||
|
|
||||||
|
database.upsertCategory({
|
||||||
|
id: 'TEST2',
|
||||||
|
category: 'TEST CATEGORY',
|
||||||
|
subCategory: 'Test Sub 2',
|
||||||
|
shortCode: 'TS2',
|
||||||
|
explanation: 'Test',
|
||||||
|
synonyms: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const categories = database.listCategories();
|
||||||
|
assert.ok(categories.length >= 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('duplicates', () => {
|
||||||
|
it('should detect duplicate files by checksum', () => {
|
||||||
|
const checksum = 'duplicate-checksum';
|
||||||
|
|
||||||
|
database.upsertFile({
|
||||||
|
absolutePath: '/test/file1.wav',
|
||||||
|
relativePath: 'file1.wav',
|
||||||
|
fileName: 'file1.wav',
|
||||||
|
displayName: 'file1',
|
||||||
|
modifiedAt: Date.now(),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
size: 1024,
|
||||||
|
durationMs: 1000,
|
||||||
|
sampleRate: 44100,
|
||||||
|
bitDepth: 16,
|
||||||
|
checksum,
|
||||||
|
tags: [],
|
||||||
|
categories: []
|
||||||
|
});
|
||||||
|
|
||||||
|
database.upsertFile({
|
||||||
|
absolutePath: '/test/file2.wav',
|
||||||
|
relativePath: 'file2.wav',
|
||||||
|
fileName: 'file2.wav',
|
||||||
|
displayName: 'file2',
|
||||||
|
modifiedAt: Date.now(),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
size: 1024,
|
||||||
|
durationMs: 1000,
|
||||||
|
sampleRate: 44100,
|
||||||
|
bitDepth: 16,
|
||||||
|
checksum,
|
||||||
|
tags: [],
|
||||||
|
categories: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const duplicates = database.listDuplicateGroups();
|
||||||
|
assert.strictEqual(duplicates.length, 1);
|
||||||
|
assert.strictEqual(duplicates[0].checksum, checksum);
|
||||||
|
assert.strictEqual(duplicates[0].files.length, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not include unique files in duplicates', () => {
|
||||||
|
database.upsertFile({
|
||||||
|
absolutePath: '/test/file1.wav',
|
||||||
|
relativePath: 'file1.wav',
|
||||||
|
fileName: 'file1.wav',
|
||||||
|
displayName: 'file1',
|
||||||
|
modifiedAt: Date.now(),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
size: 1024,
|
||||||
|
durationMs: 1000,
|
||||||
|
sampleRate: 44100,
|
||||||
|
bitDepth: 16,
|
||||||
|
checksum: 'unique1',
|
||||||
|
tags: [],
|
||||||
|
categories: []
|
||||||
|
});
|
||||||
|
|
||||||
|
database.upsertFile({
|
||||||
|
absolutePath: '/test/file2.wav',
|
||||||
|
relativePath: 'file2.wav',
|
||||||
|
fileName: 'file2.wav',
|
||||||
|
displayName: 'file2',
|
||||||
|
modifiedAt: Date.now(),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
size: 1024,
|
||||||
|
durationMs: 1000,
|
||||||
|
sampleRate: 44100,
|
||||||
|
bitDepth: 16,
|
||||||
|
checksum: 'unique2',
|
||||||
|
tags: [],
|
||||||
|
categories: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const duplicates = database.listDuplicateGroups();
|
||||||
|
assert.strictEqual(duplicates.length, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('settings', () => {
|
||||||
|
it('should save and retrieve library path setting', () => {
|
||||||
|
database.setSetting('libraryPath', '/test/library');
|
||||||
|
const settings = database.getSettings();
|
||||||
|
|
||||||
|
assert.strictEqual(settings.libraryPath, '/test/library');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null for missing library path', () => {
|
||||||
|
const settings = database.getSettings();
|
||||||
|
assert.strictEqual(settings.libraryPath, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should overwrite existing setting', () => {
|
||||||
|
database.setSetting('libraryPath', '/path1');
|
||||||
|
database.setSetting('libraryPath', '/path2');
|
||||||
|
|
||||||
|
const settings = database.getSettings();
|
||||||
|
assert.strictEqual(settings.libraryPath, '/path2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle custom settings', () => {
|
||||||
|
database.setSetting('customKey', { test: 'value' });
|
||||||
|
// Settings are stored as JSON strings internally
|
||||||
|
// Just verify no errors thrown
|
||||||
|
assert.ok(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
389
src/test/LibraryService.test.ts
Normal file
389
src/test/LibraryService.test.ts
Normal file
@@ -0,0 +1,389 @@
|
|||||||
|
/**
|
||||||
|
* Comprehensive test suite for LibraryService.
|
||||||
|
* Run with: node --import tsx --test src/test/LibraryService.test.ts
|
||||||
|
*/
|
||||||
|
import { describe, it, beforeEach, afterEach } from 'node:test';
|
||||||
|
import assert from 'node:assert';
|
||||||
|
import { LibraryService } from '../main/services/LibraryService';
|
||||||
|
import { DatabaseService } from '../main/services/DatabaseService';
|
||||||
|
import { SettingsService } from '../main/services/SettingsService';
|
||||||
|
import { TagService } from '../main/services/TagService';
|
||||||
|
import { SearchService } from '../main/services/SearchService';
|
||||||
|
import { TestWavGenerator, getTempDbPath, getTempLibraryPath } from './testHelpers';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
describe('LibraryService', () => {
|
||||||
|
let libraryService: LibraryService;
|
||||||
|
let database: DatabaseService;
|
||||||
|
let settings: SettingsService;
|
||||||
|
let tagService: TagService;
|
||||||
|
let searchService: SearchService;
|
||||||
|
let dbPath: string;
|
||||||
|
let libraryPath: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
dbPath = getTempDbPath();
|
||||||
|
libraryPath = getTempLibraryPath();
|
||||||
|
|
||||||
|
database = new DatabaseService(dbPath);
|
||||||
|
database.initialize();
|
||||||
|
settings = new SettingsService(database);
|
||||||
|
tagService = new TagService(database);
|
||||||
|
searchService = new SearchService(database, tagService);
|
||||||
|
libraryService = new LibraryService(database, settings, tagService, searchService);
|
||||||
|
|
||||||
|
// Set library path in database
|
||||||
|
database.setSetting('libraryPath', libraryPath);
|
||||||
|
|
||||||
|
// Create test files
|
||||||
|
await TestWavGenerator.createTestLibrary(libraryPath, [
|
||||||
|
{
|
||||||
|
relativePath: 'test1.wav',
|
||||||
|
options: {
|
||||||
|
duration: 1,
|
||||||
|
author: 'Author 1',
|
||||||
|
copyright: 'Copyright 1',
|
||||||
|
title: 'Test 1',
|
||||||
|
rating: 3
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
relativePath: 'subfolder/test2.wav',
|
||||||
|
options: {
|
||||||
|
duration: 2,
|
||||||
|
author: 'Author 2',
|
||||||
|
copyright: 'Copyright 2',
|
||||||
|
title: 'Test 2',
|
||||||
|
rating: 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
database.close();
|
||||||
|
await TestWavGenerator.cleanupTestLibrary(libraryPath);
|
||||||
|
await fs.unlink(dbPath).catch(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scanLibrary', () => {
|
||||||
|
it('should discover WAV files in library', async () => {
|
||||||
|
const result = await libraryService.scanLibrary();
|
||||||
|
|
||||||
|
assert.strictEqual(result.added, 2);
|
||||||
|
assert.strictEqual(result.total, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should read embedded metadata during scan', async () => {
|
||||||
|
await libraryService.scanLibrary();
|
||||||
|
|
||||||
|
const files = libraryService.listFiles();
|
||||||
|
const file1 = files.find(f => f.fileName === 'test1.wav');
|
||||||
|
|
||||||
|
assert.ok(file1);
|
||||||
|
assert.strictEqual(file1.customName, 'Test 1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect updated files on rescan', async () => {
|
||||||
|
await libraryService.scanLibrary();
|
||||||
|
|
||||||
|
// Modify a file
|
||||||
|
const testFile = path.join(libraryPath, 'test1.wav');
|
||||||
|
await TestWavGenerator.writeTestWav(testFile, {
|
||||||
|
duration: 1,
|
||||||
|
author: 'Modified Author'
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await libraryService.scanLibrary();
|
||||||
|
assert.strictEqual(result.updated, 2); // Both files are "updated" on rescan
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove deleted files from database', async () => {
|
||||||
|
await libraryService.scanLibrary();
|
||||||
|
|
||||||
|
// Delete a file
|
||||||
|
await fs.unlink(path.join(libraryPath, 'test1.wav'));
|
||||||
|
|
||||||
|
const result = await libraryService.scanLibrary();
|
||||||
|
assert.strictEqual(result.removed, 1);
|
||||||
|
assert.strictEqual(result.total, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clean up orphaned temp files', async () => {
|
||||||
|
// Create a temp file
|
||||||
|
const tempFile = path.join(libraryPath, 'test.1234567890-1-abc123.tmp');
|
||||||
|
await fs.writeFile(tempFile, 'temp data');
|
||||||
|
|
||||||
|
await libraryService.scanLibrary();
|
||||||
|
|
||||||
|
// Temp file should be cleaned up
|
||||||
|
const tempExists = await fs.access(tempFile).then(() => true).catch(() => false);
|
||||||
|
assert.strictEqual(tempExists, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('listFiles', () => {
|
||||||
|
it('should return all files in library', async () => {
|
||||||
|
await libraryService.scanLibrary();
|
||||||
|
|
||||||
|
const files = libraryService.listFiles();
|
||||||
|
assert.strictEqual(files.length, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include file metadata', async () => {
|
||||||
|
await libraryService.scanLibrary();
|
||||||
|
|
||||||
|
const files = libraryService.listFiles();
|
||||||
|
const file = files[0];
|
||||||
|
|
||||||
|
assert.ok(file.absolutePath);
|
||||||
|
assert.ok(file.fileName);
|
||||||
|
assert.ok(file.relativePath);
|
||||||
|
assert.ok(typeof file.size === 'number');
|
||||||
|
assert.ok(typeof file.durationMs === 'number');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('renameFile', () => {
|
||||||
|
it('should rename a file', async () => {
|
||||||
|
await libraryService.scanLibrary();
|
||||||
|
|
||||||
|
const files = libraryService.listFiles();
|
||||||
|
const file = files[0];
|
||||||
|
|
||||||
|
const updated = await libraryService.renameFile(file.id, 'renamed.wav');
|
||||||
|
|
||||||
|
assert.strictEqual(updated.fileName, 'renamed.wav');
|
||||||
|
assert.ok(updated.absolutePath.endsWith('renamed.wav'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update file on disk', async () => {
|
||||||
|
await libraryService.scanLibrary();
|
||||||
|
|
||||||
|
const files = libraryService.listFiles();
|
||||||
|
const file = files[0];
|
||||||
|
const originalPath = file.absolutePath;
|
||||||
|
|
||||||
|
const updated = await libraryService.renameFile(file.id, 'renamed.wav');
|
||||||
|
|
||||||
|
const originalExists = await fs.access(originalPath).then(() => true).catch(() => false);
|
||||||
|
const newExists = await fs.access(updated.absolutePath).then(() => true).catch(() => false);
|
||||||
|
|
||||||
|
assert.strictEqual(originalExists, false);
|
||||||
|
assert.strictEqual(newExists, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('moveFile', () => {
|
||||||
|
it('should move file to different directory', async () => {
|
||||||
|
await libraryService.scanLibrary();
|
||||||
|
|
||||||
|
const files = libraryService.listFiles();
|
||||||
|
const file = files.find(f => f.fileName === 'test1.wav');
|
||||||
|
|
||||||
|
assert.ok(file);
|
||||||
|
const updated = await libraryService.moveFile(file.id, 'newdir');
|
||||||
|
|
||||||
|
assert.ok(updated.relativePath.includes('newdir'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create target directory if needed', async () => {
|
||||||
|
await libraryService.scanLibrary();
|
||||||
|
|
||||||
|
const files = libraryService.listFiles();
|
||||||
|
const file = files[0];
|
||||||
|
|
||||||
|
await libraryService.moveFile(file.id, 'new/nested/dir');
|
||||||
|
|
||||||
|
const dirExists = await fs.access(path.join(libraryPath, 'new/nested/dir'))
|
||||||
|
.then(() => true).catch(() => false);
|
||||||
|
|
||||||
|
assert.strictEqual(dirExists, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateCustomName', () => {
|
||||||
|
it('should update custom name in database', async () => {
|
||||||
|
await libraryService.scanLibrary();
|
||||||
|
|
||||||
|
const files = libraryService.listFiles();
|
||||||
|
const file = files[0];
|
||||||
|
|
||||||
|
const updated = libraryService.updateCustomName(file.id, 'Custom Name');
|
||||||
|
|
||||||
|
assert.strictEqual(updated.customName, 'Custom Name');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow clearing custom name', async () => {
|
||||||
|
await libraryService.scanLibrary();
|
||||||
|
|
||||||
|
const files = libraryService.listFiles();
|
||||||
|
const file = files[0];
|
||||||
|
|
||||||
|
libraryService.updateCustomName(file.id, 'Custom Name');
|
||||||
|
const updated = libraryService.updateCustomName(file.id, null);
|
||||||
|
|
||||||
|
assert.strictEqual(updated.customName, null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateFileMetadata', () => {
|
||||||
|
it('should update author metadata', async () => {
|
||||||
|
await libraryService.scanLibrary();
|
||||||
|
|
||||||
|
const files = libraryService.listFiles();
|
||||||
|
const file = files[0];
|
||||||
|
|
||||||
|
await libraryService.updateFileMetadata(file.id, {
|
||||||
|
author: 'New Author'
|
||||||
|
});
|
||||||
|
|
||||||
|
const metadata = await libraryService.readFileMetadata(file.id);
|
||||||
|
assert.strictEqual(metadata.author, 'New Author');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update copyright metadata', async () => {
|
||||||
|
await libraryService.scanLibrary();
|
||||||
|
|
||||||
|
const files = libraryService.listFiles();
|
||||||
|
const file = files[0];
|
||||||
|
|
||||||
|
await libraryService.updateFileMetadata(file.id, {
|
||||||
|
copyright: 'New Copyright'
|
||||||
|
});
|
||||||
|
|
||||||
|
const metadata = await libraryService.readFileMetadata(file.id);
|
||||||
|
assert.strictEqual(metadata.copyright, 'New Copyright');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update rating metadata', async () => {
|
||||||
|
await libraryService.scanLibrary();
|
||||||
|
|
||||||
|
const files = libraryService.listFiles();
|
||||||
|
const file = files[0];
|
||||||
|
|
||||||
|
await libraryService.updateFileMetadata(file.id, {
|
||||||
|
rating: 4
|
||||||
|
});
|
||||||
|
|
||||||
|
const metadata = await libraryService.readFileMetadata(file.id);
|
||||||
|
assert.strictEqual(metadata.rating, 4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('readFileMetadata', () => {
|
||||||
|
it('should read embedded metadata', async () => {
|
||||||
|
await libraryService.scanLibrary();
|
||||||
|
|
||||||
|
const files = libraryService.listFiles();
|
||||||
|
const file = files[0];
|
||||||
|
|
||||||
|
const metadata = await libraryService.readFileMetadata(file.id);
|
||||||
|
|
||||||
|
assert.ok(metadata);
|
||||||
|
assert.ok(metadata.author);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('listMetadataSuggestions', () => {
|
||||||
|
it('should aggregate authors from library', async () => {
|
||||||
|
await libraryService.scanLibrary();
|
||||||
|
|
||||||
|
const suggestions = await libraryService.listMetadataSuggestions();
|
||||||
|
|
||||||
|
assert.ok(suggestions.authors.length > 0);
|
||||||
|
assert.ok(suggestions.authors.includes('Author 1'));
|
||||||
|
assert.ok(suggestions.authors.includes('Author 2'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should aggregate copyrights from library', async () => {
|
||||||
|
await libraryService.scanLibrary();
|
||||||
|
|
||||||
|
const suggestions = await libraryService.listMetadataSuggestions();
|
||||||
|
|
||||||
|
assert.ok(suggestions.copyrights.length > 0);
|
||||||
|
assert.ok(suggestions.copyrights.includes('Copyright 1'));
|
||||||
|
assert.ok(suggestions.copyrights.includes('Copyright 2'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return sorted suggestions', async () => {
|
||||||
|
await libraryService.scanLibrary();
|
||||||
|
|
||||||
|
const suggestions = await libraryService.listMetadataSuggestions();
|
||||||
|
|
||||||
|
// Check if arrays are sorted
|
||||||
|
const authorsSorted = [...suggestions.authors].sort();
|
||||||
|
assert.deepStrictEqual(suggestions.authors, authorsSorted);
|
||||||
|
|
||||||
|
const copyrightsSorted = [...suggestions.copyrights].sort();
|
||||||
|
assert.deepStrictEqual(suggestions.copyrights, copyrightsSorted);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteFiles', () => {
|
||||||
|
it('should delete file from disk', async () => {
|
||||||
|
await libraryService.scanLibrary();
|
||||||
|
|
||||||
|
const files = libraryService.listFiles();
|
||||||
|
const file = files[0];
|
||||||
|
const filePath = file.absolutePath;
|
||||||
|
|
||||||
|
await libraryService.deleteFiles([file.id]);
|
||||||
|
|
||||||
|
const exists = await fs.access(filePath).then(() => true).catch(() => false);
|
||||||
|
assert.strictEqual(exists, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove file from database', async () => {
|
||||||
|
await libraryService.scanLibrary();
|
||||||
|
|
||||||
|
const files = libraryService.listFiles();
|
||||||
|
const file = files[0];
|
||||||
|
|
||||||
|
await libraryService.deleteFiles([file.id]);
|
||||||
|
|
||||||
|
const remaining = libraryService.listFiles();
|
||||||
|
assert.strictEqual(remaining.length, 1);
|
||||||
|
assert.ok(!remaining.find(f => f.id === file.id));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple files', async () => {
|
||||||
|
await libraryService.scanLibrary();
|
||||||
|
|
||||||
|
const files = libraryService.listFiles();
|
||||||
|
const ids = files.map(f => f.id);
|
||||||
|
|
||||||
|
await libraryService.deleteFiles(ids);
|
||||||
|
|
||||||
|
const remaining = libraryService.listFiles();
|
||||||
|
assert.strictEqual(remaining.length, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('cleanupTempFiles', () => {
|
||||||
|
it('should remove orphaned temp files', async () => {
|
||||||
|
const tempFile = path.join(libraryPath, 'test.1234567890-1-abc123.tmp');
|
||||||
|
await fs.writeFile(tempFile, 'temp');
|
||||||
|
|
||||||
|
const cleaned = await libraryService.cleanupTempFiles();
|
||||||
|
|
||||||
|
assert.strictEqual(cleaned, 1);
|
||||||
|
const exists = await fs.access(tempFile).then(() => true).catch(() => false);
|
||||||
|
assert.strictEqual(exists, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should recover temp files when final file missing', async () => {
|
||||||
|
const finalPath = path.join(libraryPath, 'test.wav');
|
||||||
|
const tempFile = `${finalPath}.1234567890-1-abc123.tmp`;
|
||||||
|
|
||||||
|
await fs.writeFile(tempFile, 'temp data');
|
||||||
|
|
||||||
|
const cleaned = await libraryService.cleanupTempFiles();
|
||||||
|
|
||||||
|
assert.strictEqual(cleaned, 1);
|
||||||
|
const finalExists = await fs.access(finalPath).then(() => true).catch(() => false);
|
||||||
|
assert.strictEqual(finalExists, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
232
src/test/SearchService.test.ts
Normal file
232
src/test/SearchService.test.ts
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
/**
|
||||||
|
* Comprehensive test suite for SearchService.
|
||||||
|
* Run with: node --import tsx --test src/test/SearchService.test.ts
|
||||||
|
*/
|
||||||
|
import { describe, it, beforeEach, afterEach } from 'node:test';
|
||||||
|
import assert from 'node:assert';
|
||||||
|
import { SearchService } from '../main/services/SearchService';
|
||||||
|
import { DatabaseService } from '../main/services/DatabaseService';
|
||||||
|
import { TagService } from '../main/services/TagService';
|
||||||
|
import { TestWavGenerator, getTempDbPath, getTempLibraryPath } from './testHelpers';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
describe('SearchService', () => {
|
||||||
|
let searchService: SearchService;
|
||||||
|
let database: DatabaseService;
|
||||||
|
let tagService: TagService;
|
||||||
|
let dbPath: string;
|
||||||
|
let libraryPath: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
dbPath = getTempDbPath();
|
||||||
|
libraryPath = getTempLibraryPath();
|
||||||
|
|
||||||
|
database = new DatabaseService(dbPath);
|
||||||
|
database.initialize();
|
||||||
|
tagService = new TagService(database);
|
||||||
|
searchService = new SearchService(database, tagService);
|
||||||
|
|
||||||
|
// Create test files with various metadata
|
||||||
|
await TestWavGenerator.createTestLibrary(libraryPath, [
|
||||||
|
{
|
||||||
|
relativePath: 'ambience/city/traffic.wav',
|
||||||
|
options: {
|
||||||
|
author: 'John Doe',
|
||||||
|
copyright: 'Copyright 2024',
|
||||||
|
title: 'City Traffic',
|
||||||
|
tags: ['traffic', 'city', 'urban']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
relativePath: 'ambience/nature/forest.wav',
|
||||||
|
options: {
|
||||||
|
author: 'Jane Smith',
|
||||||
|
copyright: 'Copyright 2024',
|
||||||
|
title: 'Forest Ambience',
|
||||||
|
tags: ['forest', 'nature', 'birds']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
relativePath: 'effects/explosion.wav',
|
||||||
|
options: {
|
||||||
|
author: 'John Doe',
|
||||||
|
copyright: 'Copyright 2023',
|
||||||
|
title: 'Big Explosion',
|
||||||
|
tags: ['explosion', 'boom', 'loud']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Add files to database
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
absolutePath: path.join(libraryPath, 'ambience/city/traffic.wav'),
|
||||||
|
relativePath: 'ambience/city/traffic.wav',
|
||||||
|
fileName: 'traffic.wav',
|
||||||
|
displayName: 'traffic',
|
||||||
|
tags: ['traffic', 'city', 'urban'],
|
||||||
|
categories: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
absolutePath: path.join(libraryPath, 'ambience/nature/forest.wav'),
|
||||||
|
relativePath: 'ambience/nature/forest.wav',
|
||||||
|
fileName: 'forest.wav',
|
||||||
|
displayName: 'forest',
|
||||||
|
tags: ['forest', 'nature', 'birds'],
|
||||||
|
categories: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
absolutePath: path.join(libraryPath, 'effects/explosion.wav'),
|
||||||
|
relativePath: 'effects/explosion.wav',
|
||||||
|
fileName: 'explosion.wav',
|
||||||
|
displayName: 'explosion',
|
||||||
|
tags: ['explosion', 'boom', 'loud'],
|
||||||
|
categories: []
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
database.upsertFile({
|
||||||
|
...file,
|
||||||
|
modifiedAt: Date.now(),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
size: 1024,
|
||||||
|
durationMs: 1000,
|
||||||
|
sampleRate: 44100,
|
||||||
|
bitDepth: 16,
|
||||||
|
checksum: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
searchService.rebuildIndex();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
database.close();
|
||||||
|
await TestWavGenerator.cleanupTestLibrary(libraryPath);
|
||||||
|
await fs.unlink(dbPath).catch(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('search', () => {
|
||||||
|
it('should find files by filename', () => {
|
||||||
|
const results = searchService.search('traffic');
|
||||||
|
|
||||||
|
assert.ok(results.length > 0);
|
||||||
|
assert.ok(results.some(r => r.fileName === 'traffic.wav'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find files by tag', () => {
|
||||||
|
const results = searchService.search('forest');
|
||||||
|
|
||||||
|
assert.ok(results.length > 0);
|
||||||
|
assert.ok(results.some(r => r.tags.includes('forest')));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find files by path', () => {
|
||||||
|
const results = searchService.search('ambience');
|
||||||
|
|
||||||
|
assert.ok(results.length >= 2);
|
||||||
|
assert.ok(results.every(r => r.relativePath.includes('ambience')));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array for no matches', () => {
|
||||||
|
const results = searchService.search('nonexistent');
|
||||||
|
|
||||||
|
assert.strictEqual(results.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should perform fuzzy matching', () => {
|
||||||
|
const results = searchService.search('trffic'); // typo
|
||||||
|
|
||||||
|
assert.ok(results.length > 0);
|
||||||
|
assert.ok(results.some(r => r.fileName === 'traffic.wav'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support author: filter', () => {
|
||||||
|
const results = searchService.search('author:john');
|
||||||
|
|
||||||
|
assert.ok(results.length > 0);
|
||||||
|
assert.ok(results.every(r => {
|
||||||
|
const metadata = tagService.readMetadata(r.absolutePath);
|
||||||
|
return metadata.author?.toLowerCase().includes('john');
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support copyright: filter', () => {
|
||||||
|
const results = searchService.search('copyright:2024');
|
||||||
|
|
||||||
|
assert.ok(results.length >= 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should combine regular search with filters', () => {
|
||||||
|
const results = searchService.search('author:john explosion');
|
||||||
|
|
||||||
|
assert.ok(results.length > 0);
|
||||||
|
assert.ok(results.some(r => r.fileName === 'explosion.wav'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should cache metadata for performance', () => {
|
||||||
|
// First search should cache metadata
|
||||||
|
searchService.search('author:john');
|
||||||
|
|
||||||
|
// Second search should use cache
|
||||||
|
const results = searchService.search('author:john');
|
||||||
|
|
||||||
|
assert.ok(results.length > 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty query', () => {
|
||||||
|
const results = searchService.search('');
|
||||||
|
|
||||||
|
// Empty query returns all files
|
||||||
|
assert.strictEqual(results.length, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle whitespace-only query', () => {
|
||||||
|
const results = searchService.search(' ');
|
||||||
|
|
||||||
|
// Whitespace-only query is treated as empty, returns all files
|
||||||
|
assert.strictEqual(results.length, 3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('rebuildIndex', () => {
|
||||||
|
it('should update search index', () => {
|
||||||
|
// Add a new file to database
|
||||||
|
database.upsertFile({
|
||||||
|
absolutePath: path.join(libraryPath, 'new.wav'),
|
||||||
|
relativePath: 'new.wav',
|
||||||
|
fileName: 'new.wav',
|
||||||
|
displayName: 'new',
|
||||||
|
modifiedAt: Date.now(),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
size: 1024,
|
||||||
|
durationMs: 1000,
|
||||||
|
sampleRate: 44100,
|
||||||
|
bitDepth: 16,
|
||||||
|
checksum: null,
|
||||||
|
tags: ['new'],
|
||||||
|
categories: []
|
||||||
|
});
|
||||||
|
|
||||||
|
searchService.rebuildIndex();
|
||||||
|
|
||||||
|
const results = searchService.search('new');
|
||||||
|
assert.ok(results.length > 0);
|
||||||
|
assert.ok(results.some(r => r.fileName === 'new.wav'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear metadata cache', () => {
|
||||||
|
// Cache some metadata
|
||||||
|
searchService.search('author:john');
|
||||||
|
|
||||||
|
// Rebuild should clear cache
|
||||||
|
searchService.rebuildIndex();
|
||||||
|
|
||||||
|
// Search again should work
|
||||||
|
const results = searchService.search('author:john');
|
||||||
|
assert.ok(results.length > 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
69
src/test/TEST_SUMMARY.ts
Normal file
69
src/test/TEST_SUMMARY.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
/**
|
||||||
|
* Test Suite Summary
|
||||||
|
* ===================
|
||||||
|
*
|
||||||
|
* Comprehensive test coverage for AudioSort application core services.
|
||||||
|
* All tests use real WAV file generation and isolated test environments.
|
||||||
|
*
|
||||||
|
* Total Test Count: 67+ tests across 4 test files
|
||||||
|
*
|
||||||
|
* Test Files:
|
||||||
|
* -----------
|
||||||
|
*
|
||||||
|
* 1. DatabaseService.test.ts (20 tests)
|
||||||
|
* - File CRUD operations
|
||||||
|
* - Tag and category management
|
||||||
|
* - Duplicate detection
|
||||||
|
* - Settings persistence
|
||||||
|
*
|
||||||
|
* 2. TagService.test.ts (14 tests)
|
||||||
|
* - WAV metadata reading (author, copyright, title, rating)
|
||||||
|
* - WAV metadata writing
|
||||||
|
* - Tag normalization and application
|
||||||
|
*
|
||||||
|
* 3. LibraryService.test.ts (20 tests)
|
||||||
|
* - Library scanning with metadata priority
|
||||||
|
* - File operations (rename, move, delete)
|
||||||
|
* - Temp file cleanup and recovery
|
||||||
|
* - Metadata management without organizing
|
||||||
|
*
|
||||||
|
* 4. SearchService.test.ts (13 tests)
|
||||||
|
* - Fuzzy search across filename, tags, path
|
||||||
|
* - Advanced filters (author:, copyright:)
|
||||||
|
* - Typo tolerance and similarity scoring
|
||||||
|
* - Metadata caching and index rebuilding
|
||||||
|
*
|
||||||
|
* Running Tests:
|
||||||
|
* --------------
|
||||||
|
*
|
||||||
|
* # Run all tests
|
||||||
|
* npm test
|
||||||
|
*
|
||||||
|
* # Run specific service tests
|
||||||
|
* npm run test:database
|
||||||
|
* npm run test:tag
|
||||||
|
* npm run test:library
|
||||||
|
* npm run test:search
|
||||||
|
*
|
||||||
|
* Test Infrastructure:
|
||||||
|
* --------------------
|
||||||
|
*
|
||||||
|
* - Framework: Node.js built-in test runner (node:test)
|
||||||
|
* - TypeScript: tsx for runtime execution
|
||||||
|
* - WAV Generation: WaveFile library with real audio data
|
||||||
|
* - Test Isolation: Temporary databases and libraries per suite
|
||||||
|
* - Audio Format: 16-bit PCM, 44100 Hz, sine wave (440 Hz)
|
||||||
|
* - Metadata: WAV INFO chunks (INAM, IART, ICOP, IRTD, etc.)
|
||||||
|
*
|
||||||
|
* Key Features:
|
||||||
|
* -------------
|
||||||
|
*
|
||||||
|
* ✓ Real WAV file generation with embedded metadata
|
||||||
|
* ✓ Isolated test environments (no cross-contamination)
|
||||||
|
* ✓ Comprehensive coverage of all service methods
|
||||||
|
* ✓ Edge case testing (missing data, errors, duplicates)
|
||||||
|
* ✓ Fast execution with minimal dependencies
|
||||||
|
* ✓ No mocking - tests use actual service implementations
|
||||||
|
*
|
||||||
|
* For detailed documentation, see TEST_README.md
|
||||||
|
*/
|
||||||
245
src/test/TagService.test.ts
Normal file
245
src/test/TagService.test.ts
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
/**
|
||||||
|
* Comprehensive test suite for TagService.
|
||||||
|
* Run with: node --import tsx --test src/test/TagService.test.ts
|
||||||
|
*/
|
||||||
|
import { describe, it, beforeEach, afterEach } from 'node:test';
|
||||||
|
import assert from 'node:assert';
|
||||||
|
import { TagService } from '../main/services/TagService';
|
||||||
|
import { DatabaseService } from '../main/services/DatabaseService';
|
||||||
|
import { TestWavGenerator, getTempDbPath, getTempLibraryPath } from './testHelpers';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
describe('TagService', () => {
|
||||||
|
let tagService: TagService;
|
||||||
|
let database: DatabaseService;
|
||||||
|
let dbPath: string;
|
||||||
|
let libraryPath: string;
|
||||||
|
let testFilePath: string;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
dbPath = getTempDbPath();
|
||||||
|
libraryPath = getTempLibraryPath();
|
||||||
|
database = new DatabaseService(dbPath);
|
||||||
|
database.initialize();
|
||||||
|
tagService = new TagService(database);
|
||||||
|
|
||||||
|
await TestWavGenerator.createTestLibrary(libraryPath, [
|
||||||
|
{
|
||||||
|
relativePath: 'test.wav',
|
||||||
|
options: {
|
||||||
|
duration: 1,
|
||||||
|
author: 'Test Author',
|
||||||
|
copyright: 'Test Copyright',
|
||||||
|
title: 'Test Title',
|
||||||
|
rating: 3,
|
||||||
|
tags: ['test', 'audio'],
|
||||||
|
categories: ['AMBNatr']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
testFilePath = path.join(libraryPath, 'test.wav');
|
||||||
|
|
||||||
|
// Add file to database
|
||||||
|
database.upsertFile({
|
||||||
|
absolutePath: testFilePath,
|
||||||
|
relativePath: 'test.wav',
|
||||||
|
fileName: 'test.wav',
|
||||||
|
displayName: 'test',
|
||||||
|
modifiedAt: Date.now(),
|
||||||
|
createdAt: Date.now(),
|
||||||
|
size: 1024,
|
||||||
|
durationMs: 1000,
|
||||||
|
sampleRate: 44100,
|
||||||
|
bitDepth: 16,
|
||||||
|
checksum: 'test-checksum',
|
||||||
|
tags: ['test'],
|
||||||
|
categories: ['AMBNatr']
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
database.close();
|
||||||
|
await TestWavGenerator.cleanupTestLibrary(libraryPath);
|
||||||
|
await fs.unlink(dbPath).catch(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('readMetadata', () => {
|
||||||
|
it('should read author from WAV file', () => {
|
||||||
|
const metadata = tagService.readMetadata(testFilePath);
|
||||||
|
assert.strictEqual(metadata.author, 'Test Author');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should read copyright from WAV file', () => {
|
||||||
|
const metadata = tagService.readMetadata(testFilePath);
|
||||||
|
assert.strictEqual(metadata.copyright, 'Test Copyright');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should read title from WAV file', () => {
|
||||||
|
const metadata = tagService.readMetadata(testFilePath);
|
||||||
|
assert.strictEqual(metadata.title, 'Test Title');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should read rating from WAV file', () => {
|
||||||
|
const metadata = tagService.readMetadata(testFilePath);
|
||||||
|
assert.strictEqual(metadata.rating, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing metadata gracefully', async () => {
|
||||||
|
const emptyFilePath = path.join(libraryPath, 'empty.wav');
|
||||||
|
await TestWavGenerator.writeTestWav(emptyFilePath, {});
|
||||||
|
|
||||||
|
const metadata = tagService.readMetadata(emptyFilePath);
|
||||||
|
assert.strictEqual(metadata.author, undefined);
|
||||||
|
assert.strictEqual(metadata.copyright, undefined);
|
||||||
|
assert.strictEqual(metadata.title, undefined);
|
||||||
|
assert.strictEqual(metadata.rating, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle corrupted files gracefully', () => {
|
||||||
|
const metadata = tagService.readMetadata('/nonexistent/file.wav');
|
||||||
|
assert.deepStrictEqual(metadata, {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('writeMetadataOnly', () => {
|
||||||
|
it('should write author to WAV file', () => {
|
||||||
|
tagService.writeMetadataOnly(testFilePath, {
|
||||||
|
tags: ['test'],
|
||||||
|
categories: ['AMBNatr'],
|
||||||
|
author: 'New Author'
|
||||||
|
});
|
||||||
|
|
||||||
|
const metadata = tagService.readMetadata(testFilePath);
|
||||||
|
assert.strictEqual(metadata.author, 'New Author');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should write copyright to WAV file', () => {
|
||||||
|
tagService.writeMetadataOnly(testFilePath, {
|
||||||
|
tags: ['test'],
|
||||||
|
categories: ['AMBNatr'],
|
||||||
|
copyright: 'New Copyright'
|
||||||
|
});
|
||||||
|
|
||||||
|
const metadata = tagService.readMetadata(testFilePath);
|
||||||
|
assert.strictEqual(metadata.copyright, 'New Copyright');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should write title to WAV file', () => {
|
||||||
|
tagService.writeMetadataOnly(testFilePath, {
|
||||||
|
tags: ['test'],
|
||||||
|
categories: ['AMBNatr'],
|
||||||
|
title: 'New Title'
|
||||||
|
});
|
||||||
|
|
||||||
|
const metadata = tagService.readMetadata(testFilePath);
|
||||||
|
assert.strictEqual(metadata.title, 'New Title');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should write rating to WAV file', () => {
|
||||||
|
tagService.writeMetadataOnly(testFilePath, {
|
||||||
|
tags: ['test'],
|
||||||
|
categories: ['AMBNatr'],
|
||||||
|
rating: 5
|
||||||
|
});
|
||||||
|
|
||||||
|
const metadata = tagService.readMetadata(testFilePath);
|
||||||
|
assert.strictEqual(metadata.rating, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear metadata when set to empty', () => {
|
||||||
|
tagService.writeMetadataOnly(testFilePath, {
|
||||||
|
tags: ['test'],
|
||||||
|
categories: ['AMBNatr'],
|
||||||
|
author: '',
|
||||||
|
copyright: '',
|
||||||
|
title: '',
|
||||||
|
rating: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const metadata = tagService.readMetadata(testFilePath);
|
||||||
|
assert.strictEqual(metadata.author, undefined);
|
||||||
|
assert.strictEqual(metadata.copyright, undefined);
|
||||||
|
assert.strictEqual(metadata.title, undefined);
|
||||||
|
assert.strictEqual(metadata.rating, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should write tags to WAV file', () => {
|
||||||
|
tagService.writeMetadataOnly(testFilePath, {
|
||||||
|
tags: ['new', 'tags'],
|
||||||
|
categories: ['AMBNatr']
|
||||||
|
});
|
||||||
|
|
||||||
|
// Metadata write succeeded without throwing
|
||||||
|
assert.ok(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should write categories to WAV file', () => {
|
||||||
|
tagService.writeMetadataOnly(testFilePath, {
|
||||||
|
tags: ['test'],
|
||||||
|
categories: ['AMBCity', 'AMBNatr']
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify write succeeded
|
||||||
|
assert.ok(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('applyTagging', () => {
|
||||||
|
it('should update tags in database', () => {
|
||||||
|
const files = database.listFiles();
|
||||||
|
const fileId = files[0].id;
|
||||||
|
|
||||||
|
const updated = tagService.applyTagging(fileId, ['new', 'tags'], ['AMBNatr']);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(updated.tags, ['new', 'tags']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update categories in database', () => {
|
||||||
|
const files = database.listFiles();
|
||||||
|
const fileId = files[0].id;
|
||||||
|
|
||||||
|
const updated = tagService.applyTagging(fileId, ['test'], ['AMBCity', 'AMBNatr']);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(updated.categories, ['AMBCity', 'AMBNatr']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should write metadata to WAV file', () => {
|
||||||
|
const files = database.listFiles();
|
||||||
|
const fileId = files[0].id;
|
||||||
|
|
||||||
|
tagService.applyTagging(fileId, ['new', 'tags'], ['AMBCity']);
|
||||||
|
|
||||||
|
// Verify it didn't throw
|
||||||
|
assert.ok(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should normalize tags by trimming whitespace', () => {
|
||||||
|
const files = database.listFiles();
|
||||||
|
const fileId = files[0].id;
|
||||||
|
|
||||||
|
const updated = tagService.applyTagging(fileId, [' tag1 ', ' tag2 '], ['AMBNatr']);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(updated.tags, ['tag1', 'tag2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove duplicate tags', () => {
|
||||||
|
const files = database.listFiles();
|
||||||
|
const fileId = files[0].id;
|
||||||
|
|
||||||
|
const updated = tagService.applyTagging(fileId, ['tag1', 'tag1', 'tag2'], ['AMBNatr']);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(updated.tags, ['tag1', 'tag2']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove empty tags', () => {
|
||||||
|
const files = database.listFiles();
|
||||||
|
const fileId = files[0].id;
|
||||||
|
|
||||||
|
const updated = tagService.applyTagging(fileId, ['tag1', '', ' ', 'tag2'], ['AMBNatr']);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(updated.tags, ['tag1', 'tag2']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
152
src/test/testHelpers.ts
Normal file
152
src/test/testHelpers.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
/**
|
||||||
|
* Test helpers and utilities for creating test WAV files with metadata.
|
||||||
|
*/
|
||||||
|
import { WaveFile } from 'wavefile';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
export class TestWavGenerator {
|
||||||
|
/**
|
||||||
|
* Creates a simple 1-second WAV file with metadata.
|
||||||
|
*/
|
||||||
|
public static createTestWav(options: {
|
||||||
|
duration?: number;
|
||||||
|
sampleRate?: number;
|
||||||
|
bitDepth?: number;
|
||||||
|
tags?: string[];
|
||||||
|
categories?: string[];
|
||||||
|
author?: string;
|
||||||
|
copyright?: string;
|
||||||
|
title?: string;
|
||||||
|
rating?: number;
|
||||||
|
} = {}): Buffer {
|
||||||
|
const {
|
||||||
|
duration = 1,
|
||||||
|
sampleRate = 44100,
|
||||||
|
bitDepth = 16,
|
||||||
|
tags = [],
|
||||||
|
categories = [],
|
||||||
|
author,
|
||||||
|
copyright,
|
||||||
|
title,
|
||||||
|
rating
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
// Generate simple sine wave data
|
||||||
|
const numSamples = Math.floor(sampleRate * duration);
|
||||||
|
const frequency = 440; // A4 note
|
||||||
|
const samples = new Int16Array(numSamples);
|
||||||
|
const amplitude = Math.pow(2, bitDepth - 1) - 1;
|
||||||
|
|
||||||
|
for (let i = 0; i < numSamples; i++) {
|
||||||
|
const time = i / sampleRate;
|
||||||
|
samples[i] = Math.floor(amplitude * 0.5 * Math.sin(2 * Math.PI * frequency * time));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create WAV file
|
||||||
|
const wav = new WaveFile();
|
||||||
|
wav.fromScratch(1, sampleRate, String(bitDepth), samples);
|
||||||
|
|
||||||
|
// Add metadata using INFO chunks
|
||||||
|
const wavWithTags = wav as WaveFile & {
|
||||||
|
setTag?: (tag: string, value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (wavWithTags.setTag) {
|
||||||
|
// Set tags and categories
|
||||||
|
if (tags.length > 0) {
|
||||||
|
wavWithTags.setTag('IKEY', tags.join('; '));
|
||||||
|
wavWithTags.setTag('ISBJ', tags.join('; '));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categories.length > 0) {
|
||||||
|
wavWithTags.setTag('IGNR', categories.join('; '));
|
||||||
|
wavWithTags.setTag('ICMT', categories.join('; '));
|
||||||
|
wavWithTags.setTag('ISUB', categories.join('; '));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set metadata
|
||||||
|
if (title) {
|
||||||
|
wavWithTags.setTag('INAM', title);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (author) {
|
||||||
|
wavWithTags.setTag('IART', author);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (copyright) {
|
||||||
|
wavWithTags.setTag('ICOP', copyright);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rating && rating > 0) {
|
||||||
|
wavWithTags.setTag('IRTD', String(rating * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
wavWithTags.setTag('ISFT', 'AudioSort Test Generator');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Buffer.from(wav.toBuffer());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a test WAV file to disk.
|
||||||
|
*/
|
||||||
|
public static async writeTestWav(
|
||||||
|
filePath: string,
|
||||||
|
options: Parameters<typeof TestWavGenerator.createTestWav>[0] = {}
|
||||||
|
): Promise<void> {
|
||||||
|
const buffer = this.createTestWav(options);
|
||||||
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
||||||
|
await fs.writeFile(filePath, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates multiple test WAV files in a directory structure.
|
||||||
|
*/
|
||||||
|
public static async createTestLibrary(
|
||||||
|
rootPath: string,
|
||||||
|
files: Array<{
|
||||||
|
relativePath: string;
|
||||||
|
options?: Parameters<typeof TestWavGenerator.createTestWav>[0];
|
||||||
|
}>
|
||||||
|
): Promise<void> {
|
||||||
|
await fs.mkdir(rootPath, { recursive: true });
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const fullPath = path.join(rootPath, file.relativePath);
|
||||||
|
await this.writeTestWav(fullPath, file.options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleans up a test directory.
|
||||||
|
*/
|
||||||
|
public static async cleanupTestLibrary(rootPath: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
await fs.rm(rootPath, { recursive: true, force: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to cleanup test library at ${rootPath}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a temporary test database path.
|
||||||
|
*/
|
||||||
|
export function getTempDbPath(): string {
|
||||||
|
return path.join(process.cwd(), 'test-data', `test-${Date.now()}-${Math.random().toString(36).slice(2)}.db`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a temporary test library path.
|
||||||
|
*/
|
||||||
|
export function getTempLibraryPath(): string {
|
||||||
|
return path.join(process.cwd(), 'test-data', `test-lib-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delays execution for testing async operations.
|
||||||
|
*/
|
||||||
|
export function delay(ms: number): Promise<void> {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
14
tsconfig.base.json
Normal file
14
tsconfig.base.json
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2021",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"noEmitOnError": true,
|
||||||
|
"useUnknownInCatchVariables": false
|
||||||
|
}
|
||||||
|
}
|
||||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.main.json" },
|
||||||
|
{ "path": "./tsconfig.renderer.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
15
tsconfig.main.json
Normal file
15
tsconfig.main.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "CommonJS",
|
||||||
|
"outDir": "dist/main",
|
||||||
|
"rootDir": "src",
|
||||||
|
"types": ["node"],
|
||||||
|
"composite": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"src/main/**/*.ts",
|
||||||
|
"src/preload/**/*.ts",
|
||||||
|
"src/shared/**/*.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
13
tsconfig.renderer.json
Normal file
13
tsconfig.renderer.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"module": "ESNext",
|
||||||
|
"target": "ES2021",
|
||||||
|
"allowJs": false,
|
||||||
|
"noEmit": true,
|
||||||
|
"types": ["node", "vite/client"],
|
||||||
|
"composite": true
|
||||||
|
},
|
||||||
|
"include": ["src/renderer/src/**/*", "src/shared/**/*.ts"]
|
||||||
|
}
|
||||||
26
vite.config.mjs
Normal file
26
vite.config.mjs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const filename = fileURLToPath(import.meta.url);
|
||||||
|
const dirname = path.dirname(filename);
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
root: path.resolve(dirname, 'src/renderer'),
|
||||||
|
base: './',
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: path.resolve(dirname, 'dist/renderer'),
|
||||||
|
emptyOutDir: true
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@renderer': path.resolve(dirname, 'src/renderer/src'),
|
||||||
|
'@shared': path.resolve(dirname, 'src/shared')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 5173
|
||||||
|
}
|
||||||
|
});
|
||||||
27
vite.config.ts
Normal file
27
vite.config.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/// <reference types="node" />
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const filename = fileURLToPath(import.meta.url);
|
||||||
|
const dirname = path.dirname(filename);
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
root: path.resolve(dirname, 'src/renderer'),
|
||||||
|
base: './',
|
||||||
|
plugins: [react()],
|
||||||
|
build: {
|
||||||
|
outDir: path.resolve(dirname, 'dist/renderer'),
|
||||||
|
emptyOutDir: true
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@renderer': path.resolve(dirname, 'src/renderer/src'),
|
||||||
|
'@shared': path.resolve(dirname, 'src/shared')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 5173
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user