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 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
@@ -0,0 +1,4 @@
1
+ *.syx
2
+ *.sysx
3
+ *.sysex
4
+ *.gem
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,7 @@
1
+ module CircuitPatchTools
2
+ class Any
3
+ def self.===(*)
4
+ true
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ module CircuitPatchTools
2
+ module AttrLookup
3
+ def attr_lookup(ext, int, table)
4
+ define_method ext do
5
+ table.fetch(send(int))
6
+ end
7
+
8
+ define_method "#{ext}=" do |v|
9
+ send "#{int}=", table.index(v)
10
+ end
11
+ end
12
+ end
13
+ 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
@@ -0,0 +1,3 @@
1
+ module CircuitPatchTools
2
+ VERSION = '0.1.0'
3
+ 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: []