diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..922c364 --- /dev/null +++ b/.gitignore @@ -0,0 +1,151 @@ +# Created by https://www.toptal.com/developers/gitignore/api/goland,macos +# Edit at https://www.toptal.com/developers/gitignore?templates=goland,macos + +### GoLand ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 +.idea + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### GoLand Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +# *.iml +# modules.xml +# .idea/misc.xml +# *.ipr + +# Sonarlint plugin +# https://plugins.jetbrains.com/plugin/7973-sonarlint +.idea/**/sonarlint/ + +# SonarQube Plugin +# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin +.idea/**/sonarIssues.xml + +# Markdown Navigator plugin +# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced +.idea/**/markdown-navigator.xml +.idea/**/markdown-navigator-enh.xml +.idea/**/markdown-navigator/ + +# Cache file creation bug +# See https://youtrack.jetbrains.com/issue/JBR-2257 +.idea/$CACHE_FILE$ + +# CodeStream plugin +# https://plugins.jetbrains.com/plugin/12206-codestream +.idea/codestream.xml + +# Azure Toolkit for IntelliJ plugin +# https://plugins.jetbrains.com/plugin/8053-azure-toolkit-for-intellij +.idea/**/azureSettings.xml + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +# End of https://www.toptal.com/developers/gitignore/api/goland,macos diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..918e737 --- /dev/null +++ b/Readme.md @@ -0,0 +1,60 @@ +# Move tool + +## Overview + +A simple CLI for slicing long samples into Ableton Note / Ableton Move presets + +## Table of Contents + +- [Overview](#overview) +- [Installation](#installation) +- [Usage](#usage) +- [Contributing](#contributing) +- [License](#license) + +## Installation + +1. Ensure that you have [Go 1.22](https://golang.org/dl/) installed. +2. Clone the repo: + + ```sh + git clone https://github.com/alexfedosov/move-tool.git + ``` + +3. Navigate to the project directory: + + ```sh + cd move-tool + ``` + +4. Install dependencies: + + ```sh + go mod tidy + ``` + +## Usage + +```sh +go run . slice -i -n -o +``` + +### Example +Lets say you have prepared a long wav sample with up to 16 sounds of equal length. +To slice it up into .ablpresetbundle you need to run the tool as + +```sh +go run . slice -i my-sample.wav -n 16 -o /Users/alex/Desktop +``` + +## Contributing + +1. Fork the repository. +2. Create a new branch (`git checkout -b feature/YourFeature`). +3. Commit your changes (`git commit -m 'Add some feature'`). +4. Push to the branch (`git push origin feature/YourFeature`). +5. Open a Pull Request. + +## License + +This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. \ No newline at end of file diff --git a/ablmodels/audio.go b/ablmodels/audio.go new file mode 100644 index 0000000..90dd703 --- /dev/null +++ b/ablmodels/audio.go @@ -0,0 +1,6 @@ +package ablmodels + +type AudioFile struct { + FilePath *string + Duration float64 +} diff --git a/ablmodels/chain.go b/ablmodels/chain.go new file mode 100644 index 0000000..6242afb --- /dev/null +++ b/ablmodels/chain.go @@ -0,0 +1,22 @@ +package ablmodels + +type Chain struct { + Name string `json:"name"` + Color int `json:"color"` + Devices []interface{} `json:"devices"` + Mixer Mixer `json:"mixer"` +} + +func NewChain() *Chain { + return &Chain{ + Name: "", + Color: 2, + Devices: make([]interface{}, 0), + Mixer: *NewMixer(), + } +} + +func (c *Chain) WithDevice(device interface{}) *Chain { + c.Devices = append(c.Devices, device) + return c +} diff --git a/ablmodels/device.go b/ablmodels/device.go new file mode 100644 index 0000000..abfb723 --- /dev/null +++ b/ablmodels/device.go @@ -0,0 +1,36 @@ +package ablmodels + +type Device struct { + PresetURI interface{} `json:"presetUri"` + Kind string `json:"kind"` + Name string `json:"name"` + Parameters interface{} `json:"parameters"` + Chains []interface{} `json:"chains,omitempty"` + ReturnChains []interface{} `json:"returnChains,omitempty"` +} + +func NewDevice(kind string) *Device { + return &Device{ + PresetURI: nil, + Kind: kind, + Name: "", + Parameters: nil, + Chains: make([]interface{}, 0), + ReturnChains: make([]interface{}, 0), + } +} + +func (d *Device) AddChain(chain interface{}) *Device { + d.Chains = append(d.Chains, chain) + return d +} + +func (d *Device) AddReturnChain(chain interface{}) *Device { + d.ReturnChains = append(d.ReturnChains, chain) + return d +} + +func (d *Device) WithParameters(parameters interface{}) *Device { + d.Parameters = parameters + return d +} diff --git a/ablmodels/device_preset.go b/ablmodels/device_preset.go new file mode 100644 index 0000000..12a615c --- /dev/null +++ b/ablmodels/device_preset.go @@ -0,0 +1,26 @@ +package ablmodels + +type DevicePreset struct { + Schema string `json:"$schema"` + Device +} + +const DevicePresetSchema = "http://tech.ableton.com/schema/song/1.4.4/devicePreset.json" + +func NewDrumRackDevicePresetWithSamples(samples []AudioFile) *DevicePreset { + drumRack := NewDrumRack() + for i := 0; i < 16; i++ { + if i < len(samples) { + drumRack.AddSample(samples[i]) + } else { + drumRack.AddSample(AudioFile{ + FilePath: nil, + Duration: 0, + }) + } + } + return &DevicePreset{ + DevicePresetSchema, + *NewInstrumentRack().AddChain(NewChain().WithDevice(drumRack).WithDevice(NewSaturator())), + } +} diff --git a/ablmodels/drum_cell_chain.go b/ablmodels/drum_cell_chain.go new file mode 100644 index 0000000..0b8e4f5 --- /dev/null +++ b/ablmodels/drum_cell_chain.go @@ -0,0 +1,24 @@ +package ablmodels + +type DrumCellChain struct { + Chain + DrumZoneSettings *DrumZoneSettings `json:"drumZoneSettings"` +} + +type DrumZoneSettings struct { + ReceivingNote int `json:"receivingNote"` + SendingNote int `json:"sendingNote"` + ChokeGroup interface{} `json:"chokeGroup"` +} + +func NewDrumCellChain(padIndex int, sample AudioFile) *DrumCellChain { + chain := NewChain().WithDevice(NewDrumSampler().WithSample(sample)) + chain.Mixer = *NewMixer().WithDefaultSend() + return &DrumCellChain{ + *chain, + &DrumZoneSettings{ + SendingNote: 60, + ReceivingNote: 36 + padIndex, + }, + } +} diff --git a/ablmodels/drum_sampler.go b/ablmodels/drum_sampler.go new file mode 100644 index 0000000..576d166 --- /dev/null +++ b/ablmodels/drum_sampler.go @@ -0,0 +1,25 @@ +package ablmodels + +const DrumSamplerDeviceKind = "drumCell" + +type DrumSampler struct { + Device + DeviceData DeviceData `json:"deviceData"` +} + +type DeviceData struct { + SampleURI *string `json:"sampleUri"` +} + +func NewDrumSampler() *DrumSampler { + return &DrumSampler{ + *NewDevice(DrumSamplerDeviceKind), + DeviceData{nil}, + } +} + +func (s *DrumSampler) WithSample(file AudioFile) *DrumSampler { + s.DeviceData.SampleURI = file.FilePath + s.Parameters = DefaultDrumSamplerParameters().WithVoiceEnvelopeHold(file.Duration).WithGateMode() + return s +} diff --git a/ablmodels/drum_sampler_parameters.go b/ablmodels/drum_sampler_parameters.go new file mode 100644 index 0000000..73fde82 --- /dev/null +++ b/ablmodels/drum_sampler_parameters.go @@ -0,0 +1,109 @@ +package ablmodels + +type DrumCellParameters struct { + Effect_EightBitFilterDecay float64 `json:"Effect_EightBitFilterDecay"` + Effect_EightBitResamplingRate float64 `json:"Effect_EightBitResamplingRate"` + Effect_FmAmount float64 `json:"Effect_FmAmount"` + Effect_FmFrequency float64 `json:"Effect_FmFrequency"` + Effect_LoopLength float64 `json:"Effect_LoopLength"` + Effect_LoopOffset float64 `json:"Effect_LoopOffset"` + Effect_NoiseAmount float64 `json:"Effect_NoiseAmount"` + Effect_NoiseFrequency float64 `json:"Effect_NoiseFrequency"` + Effect_On bool `json:"Effect_On"` + Effect_PitchEnvelopeAmount float64 `json:"Effect_PitchEnvelopeAmount"` + Effect_PitchEnvelopeDecay float64 `json:"Effect_PitchEnvelopeDecay"` + Effect_PunchAmount float64 `json:"Effect_PunchAmount"` + Effect_PunchTime float64 `json:"Effect_PunchTime"` + Effect_RingModAmount float64 `json:"Effect_RingModAmount"` + Effect_RingModFrequency float64 `json:"Effect_RingModFrequency"` + Effect_StretchFactor float64 `json:"Effect_StretchFactor"` + Effect_StretchGrainSize float64 `json:"Effect_StretchGrainSize"` + Effect_SubOscAmount float64 `json:"Effect_SubOscAmount"` + Effect_SubOscFrequency float64 `json:"Effect_SubOscFrequency"` + Effect_Type string `json:"Effect_Type"` + Enabled bool `json:"Enabled"` + NotePitchBend bool `json:"NotePitchBend"` + Pan float64 `json:"Pan"` + Voice_Detune float64 `json:"Voice_Detune"` + Voice_Envelope_Attack float64 `json:"Voice_Envelope_Attack"` + Voice_Envelope_Decay float64 `json:"Voice_Envelope_Decay"` + Voice_Envelope_Hold float64 `json:"Voice_Envelope_Hold"` + Voice_Envelope_Mode string `json:"Voice_Envelope_Mode"` + Voice_Filter_Frequency float64 `json:"Voice_Filter_Frequency"` + Voice_Filter_On bool `json:"Voice_Filter_On"` + Voice_Filter_PeakGain float64 `json:"Voice_Filter_PeakGain"` + Voice_Filter_Resonance float64 `json:"Voice_Filter_Resonance"` + Voice_Filter_Type string `json:"Voice_Filter_Type"` + Voice_Gain float64 `json:"Voice_Gain"` + Voice_ModulationAmount float64 `json:"Voice_ModulationAmount"` + Voice_ModulationSource string `json:"Voice_ModulationSource"` + Voice_ModulationTarget string `json:"Voice_ModulationTarget"` + Voice_PlaybackLength float64 `json:"Voice_PlaybackLength"` + Voice_PlaybackStart float64 `json:"Voice_PlaybackStart"` + Voice_Transpose int `json:"Voice_Transpose"` + Voice_VelocityToVolume float64 `json:"Voice_VelocityToVolume"` + Volume float64 `json:"Volume"` +} + +func DefaultDrumSamplerParameters() *DrumCellParameters { + p := &DrumCellParameters{ + Effect_EightBitFilterDecay: 5.0, + Effect_EightBitResamplingRate: 14080.0, + Effect_FmAmount: 0.0, + Effect_FmFrequency: 999.9998779296876, + Effect_LoopLength: 0.30000001192092896, + Effect_LoopOffset: 0.019999997690320015, + Effect_NoiseAmount: 0.0, + Effect_NoiseFrequency: 10000.0009765625, + Effect_On: true, + Effect_PitchEnvelopeAmount: 0.0, + Effect_PitchEnvelopeDecay: 0.29999998211860657, + Effect_PunchAmount: 0.0, + Effect_PunchTime: 0.12015999853610992, + Effect_RingModAmount: 0.0, + Effect_RingModFrequency: 999.9998168945313, + Effect_StretchFactor: 1.0, + Effect_StretchGrainSize: 0.09999999403953552, + Effect_SubOscAmount: 0.0, + Effect_SubOscFrequency: 59.99999237060547, + Effect_Type: "Stretch", + Enabled: true, + NotePitchBend: true, + Pan: 0.0, + Voice_Detune: 0.0, + Voice_Envelope_Attack: 0.00009999999747378752, + Voice_Envelope_Decay: 0.3, + Voice_Envelope_Hold: 1, + Voice_Envelope_Mode: "A-S-R", + Voice_Filter_Frequency: 21999.990234375, + Voice_Filter_On: true, + Voice_Filter_PeakGain: 1.0, + Voice_Filter_Resonance: 0.0, + Voice_Filter_Type: "Lowpass", + Voice_Gain: 1.0, + Voice_ModulationAmount: 0.0, + Voice_ModulationSource: "Velocity", + Voice_ModulationTarget: "Filter", + Voice_PlaybackLength: 1.0, + Voice_PlaybackStart: 0.0, + Voice_Transpose: 0, + Voice_VelocityToVolume: 0.3499999940395355, + Volume: 0, + } + return p.WithTriggerMode() +} + +func (p *DrumCellParameters) WithGateMode() *DrumCellParameters { + p.Voice_Envelope_Mode = "A-S-R" + return p +} + +func (p *DrumCellParameters) WithTriggerMode() *DrumCellParameters { + p.Voice_Envelope_Mode = "A-H-D" + return p +} + +func (p *DrumCellParameters) WithVoiceEnvelopeHold(value float64) *DrumCellParameters { + p.Voice_Envelope_Hold = value + return p +} diff --git a/ablmodels/drumrack.go b/ablmodels/drumrack.go new file mode 100644 index 0000000..31e5087 --- /dev/null +++ b/ablmodels/drumrack.go @@ -0,0 +1,13 @@ +package ablmodels + +const DrumRackDeviceKind = "drumRack" + +type DrumRack = Device + +func NewDrumRack() *DrumRack { + return NewDevice(DrumRackDeviceKind).WithParameters(DefaultInstrumentRackParameters()).AddReturnChain(NewChain().WithDevice(NewReverb())) +} + +func (d *DrumRack) AddSample(sample AudioFile) *DrumRack { + return d.AddChain(NewDrumCellChain(len(d.Chains), sample)) +} diff --git a/ablmodels/instrument_rack.go b/ablmodels/instrument_rack.go new file mode 100644 index 0000000..95a2858 --- /dev/null +++ b/ablmodels/instrument_rack.go @@ -0,0 +1,31 @@ +package ablmodels + +type InstrumentRackParameters struct { + Enabled bool `json:"Enabled"` + Macro0 float64 `json:"Macro0"` + Macro1 float64 `json:"Macro1"` + Macro2 float64 `json:"Macro2"` + Macro3 float64 `json:"Macro3"` + Macro4 float64 `json:"Macro4"` + Macro5 float64 `json:"Macro5"` + Macro6 float64 `json:"Macro6"` + Macro7 float64 `json:"Macro7"` +} + +func DefaultInstrumentRackParameters() *InstrumentRackParameters { + return &InstrumentRackParameters{ + Enabled: true, + Macro0: 0, + Macro1: 0, + Macro2: 0, + Macro3: 0, + Macro4: 0, + Macro5: 0, + Macro6: 0, + Macro7: 0, + } +} + +func NewInstrumentRack() *Device { + return NewDevice("instrumentRack").WithParameters(DefaultInstrumentRackParameters()) +} diff --git a/ablmodels/mixer.go b/ablmodels/mixer.go new file mode 100644 index 0000000..d190142 --- /dev/null +++ b/ablmodels/mixer.go @@ -0,0 +1,32 @@ +package ablmodels + +type Mixer struct { + Pan float64 `json:"pan"` + SoloCue bool `json:"solo-cue"` + SpeakerOn bool `json:"speakerOn"` + Volume float64 `json:"volume"` + Sends []Send `json:"sends"` +} + +type Send struct { + IsEnabled bool `json:"isEnabled"` + Amount float64 `json:"amount"` +} + +func NewMixer() *Mixer { + return &Mixer{ + Pan: 0, + SoloCue: false, + SpeakerOn: true, + Volume: 0, + Sends: make([]Send, 0), + } +} + +func (m *Mixer) WithDefaultSend() *Mixer { + m.Sends = append(m.Sends, Send{ + IsEnabled: true, + Amount: 0, + }) + return m +} diff --git a/ablmodels/reverb.go b/ablmodels/reverb.go new file mode 100644 index 0000000..8847156 --- /dev/null +++ b/ablmodels/reverb.go @@ -0,0 +1,81 @@ +package ablmodels + +type ReverbParameters struct { + AllPassGain float64 `json:"AllPassGain"` + AllPassSize float64 `json:"AllPassSize"` + BandFreq float64 `json:"BandFreq"` + BandHighOn bool `json:"BandHighOn"` + BandLowOn bool `json:"BandLowOn"` + BandWidth float64 `json:"BandWidth"` + ChorusOn bool `json:"ChorusOn"` + CutOn bool `json:"CutOn"` + DecayTime float64 `json:"DecayTime"` + DiffuseDelay float64 `json:"DiffuseDelay"` + EarlyReflectModDepth float64 `json:"EarlyReflectModDepth"` + EarlyReflectModFreq float64 `json:"EarlyReflectModFreq"` + Enabled bool `json:"Enabled"` + FlatOn bool `json:"FlatOn"` + FreezeOn bool `json:"FreezeOn"` + HighFilterType string `json:"HighFilterType"` + MixDiffuse float64 `json:"MixDiffuse"` + MixDirect float64 `json:"MixDirect"` + MixReflect float64 `json:"MixReflect"` + PreDelay float64 `json:"PreDelay"` + RoomSize float64 `json:"RoomSize"` + RoomType string `json:"RoomType"` + ShelfHiFreq float64 `json:"ShelfHiFreq"` + ShelfHiGain float64 `json:"ShelfHiGain"` + ShelfHighOn bool `json:"ShelfHighOn"` + ShelfLoFreq float64 `json:"ShelfLoFreq"` + ShelfLoGain float64 `json:"ShelfLoGain"` + ShelfLowOn bool `json:"ShelfLowOn"` + SizeModDepth float64 `json:"SizeModDepth"` + SizeModFreq float64 `json:"SizeModFreq"` + SizeSmoothing string `json:"SizeSmoothing"` + SpinOn bool `json:"SpinOn"` + StereoSeparation float64 `json:"StereoSeparation"` +} + +func DefaultReverbParameters() ReverbParameters { + return ReverbParameters{ + AllPassGain: 0.6000000238418579, + AllPassSize: 0.4000000059604645, + BandFreq: 829.999755859375, + BandHighOn: false, + BandLowOn: true, + BandWidth: 5.849999904632568, + ChorusOn: true, + CutOn: true, + DecayTime: 1200.0001220703125, + DiffuseDelay: 0.5, + EarlyReflectModDepth: 17.5, + EarlyReflectModFreq: 0.29770001769065857, + Enabled: true, + FlatOn: true, + FreezeOn: false, + HighFilterType: "Shelf", + MixDiffuse: 1.0, + MixDirect: 0.550000011920929, + MixReflect: 1.0, + PreDelay: 2.5, + RoomSize: 99.99999237060548, + RoomType: "SuperEco", + ShelfHiFreq: 4500.00146484375, + ShelfHiGain: 0.699999988079071, + ShelfHighOn: true, + ShelfLoFreq: 90.0, + ShelfLoGain: 1.0, + ShelfLowOn: true, + SizeModDepth: 0.019999999552965164, + SizeModFreq: 0.020000001415610313, + SizeSmoothing: "Fast", + SpinOn: true, + StereoSeparation: 100.0, + } +} + +const ReverbDeviceKind = "reverb" + +func NewReverb() *Device { + return NewDevice(ReverbDeviceKind).WithParameters(DefaultReverbParameters()) +} diff --git a/ablmodels/saturator.go b/ablmodels/saturator.go new file mode 100644 index 0000000..6132f96 --- /dev/null +++ b/ablmodels/saturator.go @@ -0,0 +1,55 @@ +package ablmodels + +type SaturatorParameters struct { + BaseDrive float64 `json:"BaseDrive"` + BassShaperThreshold float64 `json:"BassShaperThreshold"` + ColorDepth float64 `json:"ColorDepth"` + ColorFrequency float64 `json:"ColorFrequency"` + ColorOn bool `json:"ColorOn"` + ColorWidth float64 `json:"ColorWidth"` + DryWet float64 `json:"DryWet"` + Enabled bool `json:"Enabled"` + Oversampling bool `json:"Oversampling"` + PostClip string `json:"PostClip"` + PostDrive float64 `json:"PostDrive"` + PreDcFilter bool `json:"PreDcFilter"` + PreDrive float64 `json:"PreDrive"` + Type string `json:"Type"` + WsCurve float64 `json:"WsCurve"` + WsDamp float64 `json:"WsDamp"` + WsDepth float64 `json:"WsDepth"` + WsDrive float64 `json:"WsDrive"` + WsLin float64 `json:"WsLin"` + WsPeriod float64 `json:"WsPeriod"` +} + +func DefaultSaturatorParameters() SaturatorParameters { + return SaturatorParameters{ + BaseDrive: -20.25, + BassShaperThreshold: -50.0, + ColorDepth: 0.0, + ColorFrequency: 999.9998779296876, + ColorOn: true, + ColorWidth: 0.30000001192092896, + DryWet: 0.2936508059501648, + Enabled: true, + Oversampling: true, + PostClip: "off", + PostDrive: -23.714284896850582, + PreDcFilter: false, + PreDrive: 20.571426391601563, + Type: "Soft Sine", + WsCurve: 0.05000000074505806, + WsDamp: 0.0, + WsDepth: 0.0, + WsDrive: 1.0, + WsLin: 0.5, + WsPeriod: 0.0, + } +} + +const SaturatorDeviceKind = "saturator" + +func NewSaturator() *Device { + return NewDevice(SaturatorDeviceKind).WithParameters(DefaultSaturatorParameters()) +} diff --git a/app/app.go b/app/app.go new file mode 100644 index 0000000..93fc2af --- /dev/null +++ b/app/app.go @@ -0,0 +1,41 @@ +package app + +import ( + "fmt" + "github.com/brianvoe/gofakeit/v7" + "move-tool/ablmodels" + "strings" +) + +func SliceSampleIntoDrumRack(inputFilePath string, outputFolderPath string, numberOfSlices int) (err error) { + err = gofakeit.Seed(0) + if err != nil { + return err + } + presetName := strings.ToLower(fmt.Sprintf("%s_%s", gofakeit.HipsterWord(), gofakeit.AdverbPlace())) + presetFolderPath, err := createFolderIfNotExist(outputFolderPath, presetName) + if err != nil { + return err + } + samplesFolderPath, err := createFolderIfNotExist(presetFolderPath, "Samples") + if err != nil { + return err + } + samples, err := writeAudioFileSlices(inputFilePath, samplesFolderPath, numberOfSlices) + if err != nil { + return err + } + + preset := ablmodels.NewDrumRackDevicePresetWithSamples(*samples) + + err = writePresetFile(preset, presetFolderPath) + if err != nil { + return err + } + err = archivePresetBundle(presetName, presetFolderPath, outputFolderPath) + if err != nil { + return err + } + + return nil +} diff --git a/app/audioutils.go b/app/audioutils.go new file mode 100644 index 0000000..2b20f52 --- /dev/null +++ b/app/audioutils.go @@ -0,0 +1,76 @@ +package app + +import ( + "fmt" + "github.com/go-audio/audio" + "github.com/go-audio/wav" + "move-tool/ablmodels" + "os" + "path" +) + +func writeAudioFileSlices(filePath string, outputDir string, parts int) (*[]ablmodels.AudioFile, error) { + file, err := os.Open(filePath) + if err != nil { + return nil, fmt.Errorf("could not open source file: %v", err) + } + + decoder := wav.NewDecoder(file) + decoder.ReadInfo() + + // Read the entire wave data + buf, err := decoder.FullPCMBuffer() + if err != nil { + return nil, fmt.Errorf("could not read wave data: %v", err) + } + + // Calculate the number of samples per part + samplesPerPart := len(buf.Data) / parts + + sampleDurationMilliseconds := float64(decoder.SampleRate) * float64(decoder.BitDepth) / 8 * float64(samplesPerPart) / 1000 + + result := make([]ablmodels.AudioFile, parts) + + // Loop through each part and save it as a new file + for i := 0; i < parts; i++ { + start := i * samplesPerPart + end := start + samplesPerPart + if i == parts-1 { + end = len(buf.Data) // Make sure the last part gets all remaining samples + } + + partBuffer := &audio.IntBuffer{ + Format: buf.Format, + Data: buf.Data[start:end], + } + + // Generate output file path + partFileName := fmt.Sprintf("part_%d.wav", i+1) + outputFilePath := path.Join(outputDir, partFileName) + partFile, err := os.Create(outputFilePath) + if err != nil { + return nil, fmt.Errorf("could not create part file: %v", err) + } + + // Create a new encoder + encoder := wav.NewEncoder(partFile, buf.Format.SampleRate, int(decoder.BitDepth), buf.Format.NumChannels, 1) + + if err := encoder.Write(partBuffer); err != nil { + return nil, fmt.Errorf("could not write part buffer: %v", err) + } + + if err := encoder.Close(); err != nil { + return nil, fmt.Errorf("could not close encoder: %v", err) + } + + partFile.Close() + fmt.Printf("Part %d saved as %s\n", i+1, outputFilePath) + sampleFilePath := fmt.Sprintf("Samples/%s", partFileName) + result[i] = ablmodels.AudioFile{ + FilePath: &sampleFilePath, + Duration: sampleDurationMilliseconds, + } + } + + return &result, nil +} diff --git a/app/fileutils.go b/app/fileutils.go new file mode 100644 index 0000000..ea871db --- /dev/null +++ b/app/fileutils.go @@ -0,0 +1,77 @@ +package app + +import ( + "archive/zip" + "encoding/json" + "fmt" + "io" + "move-tool/ablmodels" + "os" + "path" + "path/filepath" + "strings" +) + +func createFolderIfNotExist(basePath string, folderName string) (path string, err error) { + path = filepath.Join(basePath, folderName) + _, err = os.Stat(path) + if os.IsNotExist(err) { + err = os.Mkdir(path, os.ModePerm) + } + return path, err +} + +func archivePresetBundle(presetName string, directory string, output string) error { + zipFilePath := path.Join(output, fmt.Sprintf("%s.ablpresetbundle", presetName)) + fmt.Printf("Creating preset bundle %s\n", zipFilePath) + zipFile, err := os.Create(zipFilePath) + if err != nil { + return err + } + defer zipFile.Close() + zipWriter := zip.NewWriter(zipFile) + defer zipWriter.Close() + err = filepath.Walk(directory, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + relativePath := strings.TrimPrefix(path, fmt.Sprintf("%s/", directory)) + if info.IsDir() { + return nil + } + zipFileWriter, err := zipWriter.Create(relativePath) + if err != nil { + return err + } + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + _, err = io.Copy(zipFileWriter, file) + if err != nil { + return err + } + return nil + }) + return err +} + +func writePresetFile(preset *ablmodels.DevicePreset, presetFolderPath string) error { + presentJSON, err := json.MarshalIndent(preset, "", " ") + if err != nil { + return err + } + filePath := fmt.Sprintf("%s/Preset.ablpreset", presetFolderPath) + file, err := os.Create(filePath) + if err != nil { + return err + } + defer file.Close() + + _, err = file.Write(presentJSON) + if err != nil { + return err + } + return nil +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..769e2e3 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,17 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +var ( + rootCmd = &cobra.Command{ + Use: "move-tool", + Short: "move-tool helps you mangle your Ableton Move files.", + Long: `move-tool is a CLI for mangling your Ableton Move files.`, + } +) + +func Execute() error { + return rootCmd.Execute() +} diff --git a/cmd/slice.go b/cmd/slice.go new file mode 100644 index 0000000..7bbc1ed --- /dev/null +++ b/cmd/slice.go @@ -0,0 +1,31 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "move-tool/app" +) + +var ( + input string + output string + numberOfSlices int + + sliceCmd = &cobra.Command{ + Use: "slice", + Short: "Slices long sample into drum rack", + Long: `Slice long sample into given number of equal numberOfSlices and creates a drum rack preset`, + RunE: func(cmd *cobra.Command, args []string) error { + return app.SliceSampleIntoDrumRack(input, output, numberOfSlices) + }, + } +) + +func init() { + sliceCmd.Flags().StringVarP(&input, "input", "i", "", "input file") + _ = sliceCmd.MarkFlagRequired("input") + sliceCmd.Flags().StringVarP(&output, "output", "o", "", "Output directory") + _ = sliceCmd.MarkFlagRequired("output") + sliceCmd.Flags().IntVarP(&numberOfSlices, "numberOfSlices", "n", 16, "Number of numberOfSlices") + _ = sliceCmd.MarkFlagRequired("numberOfSlices") + rootCmd.AddCommand(sliceCmd) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7c4fc95 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module move-tool + +go 1.22 + +require ( + github.com/brianvoe/gofakeit/v7 v7.0.4 + github.com/go-audio/audio v1.0.0 + github.com/go-audio/wav v1.1.0 + github.com/spf13/cobra v1.8.1 +) + +require ( + github.com/go-audio/riff v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f69e3a5 --- /dev/null +++ b/go.sum @@ -0,0 +1,18 @@ +github.com/brianvoe/gofakeit/v7 v7.0.4 h1:Mkxwz9jYg8Ad8NvT9HA27pCMZGFQo08MK6jD0QTKEww= +github.com/brianvoe/gofakeit/v7 v7.0.4/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/go-audio/audio v1.0.0 h1:zS9vebldgbQqktK4H0lUqWrG8P0NxCJVqcj7ZpNnwd4= +github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= +github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA= +github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498= +github.com/go-audio/wav v1.1.0 h1:jQgLtbqBzY7G+BM8fXF7AHUk1uHUviWS4X39d5rsL2g= +github.com/go-audio/wav v1.1.0/go.mod h1:mpe9qfwbScEbkd8uybLuIpTgHyrISw/OTuvjUW2iGtE= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..953fda2 --- /dev/null +++ b/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "fmt" + "move-tool/cmd" +) + +func main() { + err := cmd.Execute() + if err != nil { + fmt.Println("Error:", err) + } +}