circuit_patch_tools 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/LICENSE +15 -0
- data/README.md +12 -0
- data/bin/circuit-patch +26 -0
- data/circuit_patch_tools.gemspec +23 -0
- data/lib/circuit_patch_tools/any.rb +7 -0
- data/lib/circuit_patch_tools/attr_lookup.rb +13 -0
- data/lib/circuit_patch_tools/commands.rb +12 -0
- data/lib/circuit_patch_tools/commands/info.rb +71 -0
- data/lib/circuit_patch_tools/commands/join.rb +60 -0
- data/lib/circuit_patch_tools/commands/portable.rb +59 -0
- data/lib/circuit_patch_tools/commands/split.rb +73 -0
- data/lib/circuit_patch_tools/patch.rb +80 -0
- data/lib/circuit_patch_tools/version.rb +3 -0
- metadata +58 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 51b6d91974fadb28cedb4b53697392bb2d89997853ba2066c014ba867dc1a33c
|
4
|
+
data.tar.gz: d6d8a203ccb8dd9e536a1b4b79e9226fc347c9d1cd061c52ba990330de4fb1aa
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1d1dfc98ec4fb9dd344c54bde3d7c82efd9e73c8963dedc2a4f42dbdf8e16dbdedfd7f0c1afd5993b0b26c8a066dc10deb1a81a1ba6ecdcde62dc69787a51ad8
|
7
|
+
data.tar.gz: be3fd12bcbcdbf51c3673b837d6050f0c429464472a2fde35423ad49493c5675d365a76ca4c5500b98bdf68bd7f752d42f6cd5f6656c19c578b4ded695c94faa
|
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
ISC License (ISC)
|
2
|
+
|
3
|
+
Copyright 2019 Paul Battley
|
4
|
+
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
8
|
+
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
10
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
11
|
+
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
12
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
13
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
14
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
15
|
+
PERFORMANCE OF THIS SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# Circuit Patch Tools
|
2
|
+
|
3
|
+
Tools for manipulating Novation Circuit patches:
|
4
|
+
|
5
|
+
- `split` an set of patches into individual files
|
6
|
+
- make them `portable` so that they can be auditioned as the current patch
|
7
|
+
- print `info` about a single patch file
|
8
|
+
- `join` individual files into a bank
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
gem install circuit_patch_tools
|
data/bin/circuit-patch
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'circuit_patch_tools/commands'
|
4
|
+
require 'optparse'
|
5
|
+
|
6
|
+
handlers = CircuitPatchTools::Commands.handlers
|
7
|
+
handler = handlers.find { |h| h.name == ARGV.first }
|
8
|
+
if handler
|
9
|
+
handler.run ARGV.drop(1)
|
10
|
+
else
|
11
|
+
OptionParser.new do |opts|
|
12
|
+
opts.banner = <<~END
|
13
|
+
circuit-patch
|
14
|
+
|
15
|
+
Usage: circuit-patch command [command-options] ...
|
16
|
+
|
17
|
+
Commands:
|
18
|
+
#{handlers.map { |c| " #{c.name}" }.join("\n")}
|
19
|
+
|
20
|
+
Options:
|
21
|
+
END
|
22
|
+
opts.on('-h', '--help', 'Print this help') do
|
23
|
+
end
|
24
|
+
puts opts
|
25
|
+
end.parse!
|
26
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
lib = File.expand_path("../lib", __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require "circuit_patch_tools/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "circuit_patch_tools"
|
7
|
+
spec.version = CircuitPatchTools::VERSION
|
8
|
+
spec.authors = ["Paul Battley"]
|
9
|
+
spec.email = ["pbattley@gmail.com"]
|
10
|
+
spec.license = "ISC"
|
11
|
+
|
12
|
+
spec.summary = %q{Tools for manipulating Novation Circuit patches}
|
13
|
+
spec.homepage = "https://github.com/threedaymonk/circuit_patch_tools"
|
14
|
+
|
15
|
+
# Specify which files should be added to the gem when it is released.
|
16
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
17
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
18
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
19
|
+
end
|
20
|
+
spec.bindir = "bin"
|
21
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'circuit_patch_tools/commands/info'
|
2
|
+
require 'circuit_patch_tools/commands/join'
|
3
|
+
require 'circuit_patch_tools/commands/portable'
|
4
|
+
require 'circuit_patch_tools/commands/split'
|
5
|
+
|
6
|
+
module CircuitPatchTools
|
7
|
+
module Commands
|
8
|
+
def self.handlers
|
9
|
+
@handlers ||= [Info, Join, Portable, Split].map(&:new)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'circuit_patch_tools/patch'
|
2
|
+
require 'optparse'
|
3
|
+
|
4
|
+
module CircuitPatchTools
|
5
|
+
module Commands
|
6
|
+
class Info
|
7
|
+
FIELDS = %i[ path name command location genre category polyphony ]
|
8
|
+
|
9
|
+
DEFAULT_OPTIONS = {
|
10
|
+
fields: %w[ name genre category command location polyphony ]
|
11
|
+
}
|
12
|
+
|
13
|
+
def name
|
14
|
+
'info'
|
15
|
+
end
|
16
|
+
|
17
|
+
def description
|
18
|
+
'show patch information'
|
19
|
+
end
|
20
|
+
|
21
|
+
def run(args)
|
22
|
+
options = DEFAULT_OPTIONS.dup
|
23
|
+
|
24
|
+
OptionParser.new do |opts|
|
25
|
+
opts.banner = <<~END
|
26
|
+
#{name}: #{description}
|
27
|
+
|
28
|
+
Usage: circuit-patch #{name} [options] patch1.sysex [patch2.sysex ...]
|
29
|
+
|
30
|
+
Options:
|
31
|
+
END
|
32
|
+
opts.on('-fFIELDS', '--fields=FIELDS',
|
33
|
+
'Comma-separated list of fields to show',
|
34
|
+
"Default: #{DEFAULT_OPTIONS.fetch(:fields).join(',')}",
|
35
|
+
Array) do |v|
|
36
|
+
options[:fields] = v
|
37
|
+
end
|
38
|
+
opts.on('-l', '--list', 'List available fields') do
|
39
|
+
puts *FIELDS
|
40
|
+
end
|
41
|
+
opts.on('-h', '--help', 'Print this help') do
|
42
|
+
puts opts
|
43
|
+
return
|
44
|
+
end
|
45
|
+
end.parse!(args)
|
46
|
+
|
47
|
+
args.each do |path|
|
48
|
+
show_info options, path
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
def show_info(options, path)
|
54
|
+
patch = Patch.open(path)
|
55
|
+
metadata = FIELDS.map { |f| [f.to_s, patch.send(f)] }.to_h
|
56
|
+
options.fetch(:fields).each do |k|
|
57
|
+
puts "#{k}: #{metadata.fetch(k)}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class Patch < CircuitPatchTools::Patch
|
62
|
+
attr_accessor :path
|
63
|
+
|
64
|
+
def self.open(path)
|
65
|
+
raw = File.open(path, 'rb', encoding: Encoding::ASCII_8BIT).read
|
66
|
+
unpack(raw).tap { |p| p.path = path }
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'circuit_patch_tools/patch'
|
2
|
+
require 'optparse'
|
3
|
+
|
4
|
+
module CircuitPatchTools
|
5
|
+
module Commands
|
6
|
+
class Join
|
7
|
+
DEFAULT_OPTIONS = {
|
8
|
+
output: 'patches.sysex'
|
9
|
+
}
|
10
|
+
|
11
|
+
def name
|
12
|
+
'join'
|
13
|
+
end
|
14
|
+
|
15
|
+
def description
|
16
|
+
'join up to 64 patches into a single sysex file'
|
17
|
+
end
|
18
|
+
|
19
|
+
def run(args)
|
20
|
+
options = DEFAULT_OPTIONS.dup
|
21
|
+
|
22
|
+
OptionParser.new do |opts|
|
23
|
+
opts.banner = <<~END
|
24
|
+
#{name}: #{description}
|
25
|
+
|
26
|
+
Usage: circuit-patch #{name} [options] patch1.sysex [patch2.sysex ...]
|
27
|
+
|
28
|
+
Options:
|
29
|
+
END
|
30
|
+
opts.on('-oFILENAME', '--output=FILENAME',
|
31
|
+
'Output filename',
|
32
|
+
"Default: #{DEFAULT_OPTIONS[:output]}",
|
33
|
+
String) do |v|
|
34
|
+
options[:output] = v
|
35
|
+
end
|
36
|
+
opts.on('-h', '--help', 'Print this help') do
|
37
|
+
puts opts
|
38
|
+
return
|
39
|
+
end
|
40
|
+
end.parse!(args)
|
41
|
+
|
42
|
+
join options, args
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def join(options, paths)
|
47
|
+
output = options.fetch(:output)
|
48
|
+
File.open(output, 'wb', encoding: Encoding::ASCII_8BIT) do |f|
|
49
|
+
paths.each.with_index do |path, index|
|
50
|
+
raw = File.open(path, 'rb', encoding: Encoding::ASCII_8BIT).read
|
51
|
+
patch = Patch.unpack(raw)
|
52
|
+
patch.command = :replace_patch
|
53
|
+
patch.location = index
|
54
|
+
f << patch.pack
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'circuit_patch_tools/patch'
|
2
|
+
require 'optparse'
|
3
|
+
|
4
|
+
module CircuitPatchTools
|
5
|
+
module Commands
|
6
|
+
class Portable
|
7
|
+
DEFAULT_OPTIONS = {
|
8
|
+
synth: 1
|
9
|
+
}
|
10
|
+
|
11
|
+
def name
|
12
|
+
'portable'
|
13
|
+
end
|
14
|
+
|
15
|
+
def description
|
16
|
+
'make patches portable'
|
17
|
+
end
|
18
|
+
|
19
|
+
def run(args)
|
20
|
+
options = DEFAULT_OPTIONS.dup
|
21
|
+
|
22
|
+
OptionParser.new do |opts|
|
23
|
+
opts.banner = <<~END
|
24
|
+
#{name}: #{description}
|
25
|
+
|
26
|
+
Usage: circuit-patch #{name} [options] patch1.sysex [patch2.sysex ...]
|
27
|
+
|
28
|
+
Options:
|
29
|
+
END
|
30
|
+
opts.on('-sSYNTH', '--synth=SYNTH',
|
31
|
+
'Synth for the patch',
|
32
|
+
"Default: #{DEFAULT_OPTIONS[:synth]}",
|
33
|
+
Integer) do |v|
|
34
|
+
options[:synth] = v
|
35
|
+
end
|
36
|
+
opts.on('-h', '--help', 'Print this help') do
|
37
|
+
puts opts
|
38
|
+
return
|
39
|
+
end
|
40
|
+
end.parse!(args)
|
41
|
+
|
42
|
+
args.each do |path|
|
43
|
+
make_portable options, path
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
def make_portable(options, path)
|
49
|
+
raw = File.open(path, 'rb', encoding: Encoding::ASCII_8BIT).read
|
50
|
+
patch = Patch.unpack(raw)
|
51
|
+
patch.command = :replace_current_patch
|
52
|
+
patch.location = options.fetch(:synth) - 1
|
53
|
+
File.open(path, 'wb', encoding: Encoding::ASCII_8BIT) do |f|
|
54
|
+
f << patch.pack
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'circuit_patch_tools/patch'
|
2
|
+
require 'optparse'
|
3
|
+
|
4
|
+
module CircuitPatchTools
|
5
|
+
module Commands
|
6
|
+
class Split
|
7
|
+
FIELDS = %i[ name command location genre category polyphony ]
|
8
|
+
|
9
|
+
DEFAULT_OPTIONS = {
|
10
|
+
filename: '%<location>02d - %<name>s.sysex'
|
11
|
+
}
|
12
|
+
|
13
|
+
PATCH_REGEXP = /\xF0\x00\x20\x29\x01\x60[\x00-\xFF]{343}\xF7/n
|
14
|
+
|
15
|
+
def name
|
16
|
+
'split'
|
17
|
+
end
|
18
|
+
|
19
|
+
def description
|
20
|
+
'extract patches from a single sysex file into one file per patch'
|
21
|
+
end
|
22
|
+
|
23
|
+
def run(args)
|
24
|
+
options = DEFAULT_OPTIONS.dup
|
25
|
+
|
26
|
+
OptionParser.new do |opts|
|
27
|
+
opts.banner = <<~END
|
28
|
+
#{name}: #{description}
|
29
|
+
|
30
|
+
Usage: circuit-patch #{name} [options] patch1.sysex [patch2.sysex ...]
|
31
|
+
|
32
|
+
Options:
|
33
|
+
END
|
34
|
+
opts.on('-fPATTERN', '--filename=PATTERN',
|
35
|
+
'Pattern for filenames',
|
36
|
+
"Default: #{DEFAULT_OPTIONS[:filename].inspect}",
|
37
|
+
String) do |v|
|
38
|
+
options[:filename] = v
|
39
|
+
end
|
40
|
+
opts.on('-h', '--help', 'Print this help') do
|
41
|
+
puts opts
|
42
|
+
return
|
43
|
+
end
|
44
|
+
end.parse!(args)
|
45
|
+
|
46
|
+
extract_all options, args
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
def extract_all(options, paths)
|
51
|
+
paths.each do |path|
|
52
|
+
File.open(path, 'rb', encoding: Encoding::ASCII_8BIT)
|
53
|
+
.read
|
54
|
+
.scan(PATCH_REGEXP).each do |raw|
|
55
|
+
extract_one options, raw
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def extract_one(options, raw)
|
61
|
+
patch = Patch.unpack(raw)
|
62
|
+
|
63
|
+
metadata = FIELDS.map { |f| [f, patch.send(f)] }.to_h
|
64
|
+
filename = format(options.fetch(:filename), metadata)
|
65
|
+
|
66
|
+
$stderr.puts filename
|
67
|
+
File.open(filename, 'wb', encoding: Encoding::ASCII_8BIT) do |f|
|
68
|
+
f << raw
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
require 'circuit_patch_tools/attr_lookup'
|
2
|
+
require 'circuit_patch_tools/any'
|
3
|
+
|
4
|
+
module CircuitPatchTools
|
5
|
+
class Patch
|
6
|
+
extend AttrLookup
|
7
|
+
|
8
|
+
SYSEX = [
|
9
|
+
['C', :sysex, 0xF0],
|
10
|
+
['C', :manufacturer_1, 0x00],
|
11
|
+
['C', :manufacturer_2, 0x20],
|
12
|
+
['C', :manufacturer_3, 0x29],
|
13
|
+
['C', :product_type, 0x01],
|
14
|
+
['C', :product_number, 0x60],
|
15
|
+
['C', :command, 0..1],
|
16
|
+
['C', :location, 0..63],
|
17
|
+
['C', :reserved, Any],
|
18
|
+
['A16', :patch_name, Any],
|
19
|
+
['C', :patch_category, 0..14],
|
20
|
+
['C', :patch_genre, 0..9],
|
21
|
+
['a14', :patch_reserved, Any],
|
22
|
+
['C', :voice_polyphony_mode, 0..2],
|
23
|
+
['a307', :patch_settings, Any],
|
24
|
+
['C', :eox, 0xF7]
|
25
|
+
]
|
26
|
+
|
27
|
+
PATTERN = SYSEX.map { |a, _, _| a }.join('')
|
28
|
+
FIELDS = SYSEX.map { |_, a, _| a }
|
29
|
+
|
30
|
+
POLYPHONY = %i[ mono mono_ag poly ]
|
31
|
+
GENRES = %i[
|
32
|
+
none classic drumbass house industrial jazz randb rock techno dubstep
|
33
|
+
]
|
34
|
+
CATEGORIES = %i[
|
35
|
+
none arp bass bell classic drum keyboard lead movement pad poly sfx
|
36
|
+
string user voc
|
37
|
+
]
|
38
|
+
COMMANDS = %i[ replace_current_patch replace_patch ]
|
39
|
+
|
40
|
+
def self.unpack(raw)
|
41
|
+
values = raw.unpack(PATTERN)
|
42
|
+
|
43
|
+
values.zip(SYSEX).each do |v, (_, name, validator)|
|
44
|
+
unless validator === v
|
45
|
+
raise "#{name}: #{v.inspect} does not satisfy #{validator.inspect}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
new(FIELDS.zip(values).to_h)
|
50
|
+
end
|
51
|
+
|
52
|
+
def initialize(parameter_hash)
|
53
|
+
@parameters = parameter_hash
|
54
|
+
end
|
55
|
+
|
56
|
+
def pack
|
57
|
+
FIELDS.map { |f| @parameters[f] }.pack(PATTERN)
|
58
|
+
end
|
59
|
+
|
60
|
+
FIELDS.each do |f|
|
61
|
+
define_method "_#{f}" do
|
62
|
+
@parameters.fetch(f)
|
63
|
+
end
|
64
|
+
|
65
|
+
define_method "_#{f}=" do |v|
|
66
|
+
@parameters[f] = v
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
alias_method :name, :_patch_name
|
71
|
+
alias_method :name=, :_patch_name=
|
72
|
+
alias_method :location, :_location
|
73
|
+
alias_method :location=, :_location=
|
74
|
+
|
75
|
+
attr_lookup :polyphony, :_voice_polyphony_mode, POLYPHONY
|
76
|
+
attr_lookup :genre, :_patch_genre, GENRES
|
77
|
+
attr_lookup :category, :_patch_category, CATEGORIES
|
78
|
+
attr_lookup :command, :_command, COMMANDS
|
79
|
+
end
|
80
|
+
end
|
metadata
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: circuit_patch_tools
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Paul Battley
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-09-01 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email:
|
15
|
+
- pbattley@gmail.com
|
16
|
+
executables:
|
17
|
+
- circuit-patch
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- ".gitignore"
|
22
|
+
- LICENSE
|
23
|
+
- README.md
|
24
|
+
- bin/circuit-patch
|
25
|
+
- circuit_patch_tools.gemspec
|
26
|
+
- lib/circuit_patch_tools/any.rb
|
27
|
+
- lib/circuit_patch_tools/attr_lookup.rb
|
28
|
+
- lib/circuit_patch_tools/commands.rb
|
29
|
+
- lib/circuit_patch_tools/commands/info.rb
|
30
|
+
- lib/circuit_patch_tools/commands/join.rb
|
31
|
+
- lib/circuit_patch_tools/commands/portable.rb
|
32
|
+
- lib/circuit_patch_tools/commands/split.rb
|
33
|
+
- lib/circuit_patch_tools/patch.rb
|
34
|
+
- lib/circuit_patch_tools/version.rb
|
35
|
+
homepage: https://github.com/threedaymonk/circuit_patch_tools
|
36
|
+
licenses:
|
37
|
+
- ISC
|
38
|
+
metadata: {}
|
39
|
+
post_install_message:
|
40
|
+
rdoc_options: []
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '0'
|
53
|
+
requirements: []
|
54
|
+
rubygems_version: 3.0.3
|
55
|
+
signing_key:
|
56
|
+
specification_version: 4
|
57
|
+
summary: Tools for manipulating Novation Circuit patches
|
58
|
+
test_files: []
|