foxtracker 0.1.0.pre1337

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 146db4596b171e89640d7f55ceec69e16ef3e5075bbafc37429d9d9aed610528
4
+ data.tar.gz: 2a72fb658ec749043b0c1b3baf306f03c40fcb2f9aaa3a3f3a8c4cd7b49137ec
5
+ SHA512:
6
+ metadata.gz: 2e7953b85f6b82728dc1ba2e45961b3f6512b1608ed7bccecb25a6969ad37bac478c8f0e39172eb94d63b8f867621988f984162524ab43946e26490887fc7fec
7
+ data.tar.gz: 9f47153f4a02d1a4a00303fae9083351a0819632ca48725ae0d8ba95529706ea5af185f86c6df64c94406250a714bffcc486d09a1426beb134d9dd0b252472f9
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,6 @@
1
+ ---
2
+ require:
3
+ - rt_rubocop_defaults
4
+
5
+ AllCops:
6
+ TargetRubyVersion: 2.5
@@ -0,0 +1 @@
1
+ ruby-2.5.1
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.5.1
5
+ before_install: gem install bundler -v 1.16.2
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at nilsding@nilsding.org. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
6
+
7
+ # Specify your gem's dependencies in foxtracker.gemspec
8
+ gemspec
@@ -0,0 +1,84 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ foxtracker (0.1.0.pre1337)
5
+ dry-struct (~> 0.5)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ ast (2.4.0)
11
+ concurrent-ruby (1.0.5)
12
+ diff-lcs (1.3)
13
+ dry-configurable (0.7.0)
14
+ concurrent-ruby (~> 1.0)
15
+ dry-container (0.6.0)
16
+ concurrent-ruby (~> 1.0)
17
+ dry-configurable (~> 0.1, >= 0.1.3)
18
+ dry-core (0.4.7)
19
+ concurrent-ruby (~> 1.0)
20
+ dry-equalizer (0.2.1)
21
+ dry-inflector (0.1.2)
22
+ dry-logic (0.4.2)
23
+ dry-container (~> 0.2, >= 0.2.6)
24
+ dry-core (~> 0.2)
25
+ dry-equalizer (~> 0.2)
26
+ dry-struct (0.5.0)
27
+ dry-core (~> 0.4, >= 0.4.3)
28
+ dry-equalizer (~> 0.2)
29
+ dry-types (~> 0.13)
30
+ ice_nine (~> 0.11)
31
+ dry-types (0.13.2)
32
+ concurrent-ruby (~> 1.0)
33
+ dry-container (~> 0.3)
34
+ dry-core (~> 0.4, >= 0.4.4)
35
+ dry-equalizer (~> 0.2)
36
+ dry-inflector (~> 0.1, >= 0.1.2)
37
+ dry-logic (~> 0.4, >= 0.4.2)
38
+ ice_nine (0.11.2)
39
+ jaro_winkler (1.5.1)
40
+ parallel (1.12.1)
41
+ parser (2.5.1.2)
42
+ ast (~> 2.4.0)
43
+ powerpack (0.1.2)
44
+ rainbow (3.0.0)
45
+ rake (10.5.0)
46
+ rspec (3.7.0)
47
+ rspec-core (~> 3.7.0)
48
+ rspec-expectations (~> 3.7.0)
49
+ rspec-mocks (~> 3.7.0)
50
+ rspec-core (3.7.1)
51
+ rspec-support (~> 3.7.0)
52
+ rspec-expectations (3.7.0)
53
+ diff-lcs (>= 1.2.0, < 2.0)
54
+ rspec-support (~> 3.7.0)
55
+ rspec-mocks (3.7.0)
56
+ diff-lcs (>= 1.2.0, < 2.0)
57
+ rspec-support (~> 3.7.0)
58
+ rspec-support (3.7.1)
59
+ rt_rubocop_defaults (1.0.0)
60
+ rubocop (~> 0.46)
61
+ rubocop (0.58.1)
62
+ jaro_winkler (~> 1.5.1)
63
+ parallel (~> 1.10)
64
+ parser (>= 2.5, != 2.5.1.1)
65
+ powerpack (~> 0.1)
66
+ rainbow (>= 2.2.2, < 4.0)
67
+ ruby-progressbar (~> 1.7)
68
+ unicode-display_width (~> 1.0, >= 1.0.1)
69
+ ruby-progressbar (1.9.0)
70
+ unicode-display_width (1.4.0)
71
+
72
+ PLATFORMS
73
+ ruby
74
+
75
+ DEPENDENCIES
76
+ bundler (~> 1.16)
77
+ foxtracker!
78
+ rake (~> 10.0)
79
+ rspec (~> 3.0)
80
+ rt_rubocop_defaults (~> 1.0)
81
+ rubocop (~> 0.58)
82
+
83
+ BUNDLED WITH
84
+ 1.16.3
@@ -0,0 +1,24 @@
1
+ Copyright (c) 2018, Georg Gadinger <nilsding@nilsding.org>
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+
10
+ * Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+
14
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
15
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
18
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
19
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
20
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
21
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
22
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
23
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24
+
@@ -0,0 +1,61 @@
1
+ # Foxtracker
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/foxtracker.svg)](https://badge.fury.io/rb/foxtracker)
4
+
5
+ Foxtracker is a parser for tracker music formats. Right now it only supports XM
6
+ (FastTracker II) modules. Support for more formats is to be done.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'foxtracker'
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ $ bundle
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install foxtracker
23
+
24
+ ## Usage
25
+
26
+ ```ruby
27
+ require "foxtracker/parser"
28
+
29
+ xm = Foxtracker::Parser.read(
30
+ # path to xm file:
31
+ File.expand_path("./siuperdu[perxmldsosnmg v2.xm", __dir__),
32
+ # display debug output during parsing (default false)
33
+ debug: true
34
+ )
35
+ #=> #<Foxtracker::Format::ExtendedModule title="superdupersongxmldng" tracker="MilkyTracker 1.00.00" ...>
36
+
37
+ xm
38
+ .patterns.first # the pattern 0
39
+ .channels.first # the first channel
40
+ .first # the first row/note for the channel
41
+ .note #=> 85 (C-7); the note value
42
+
43
+ ```
44
+
45
+ ## Development
46
+
47
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
48
+
49
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
50
+
51
+ ## Contributing
52
+
53
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/foxtracker. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
54
+
55
+ ## License
56
+
57
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
58
+
59
+ ## Code of Conduct
60
+
61
+ Everyone interacting in the Foxtracker project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/foxtracker/blob/master/CODE_OF_CONDUCT.md).
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "foxtracker"
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require "irb"
15
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "foxtracker"
5
+
6
+ Foxtracker::Application.new(ARGV).run
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path("lib", __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require "foxtracker/version"
6
+
7
+ Gem::Specification.new do |spec|
8
+ spec.name = "foxtracker"
9
+ spec.version = Foxtracker::VERSION
10
+ spec.authors = ["Georg Gadinger"]
11
+ spec.email = ["nilsding@nilsding.org"]
12
+
13
+ spec.summary = "a parser for tracker music formats"
14
+ spec.description = "Foxtracker is a parser for tracker music formats. Right now it only supports XM (FastTracker II) modules. Support for more formats is to be done."
15
+ spec.homepage = "https://github.com/nilsding/foxtracker"
16
+ spec.license = "MIT"
17
+
18
+ # Specify which files should be added to the gem when it is released.
19
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
20
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
21
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
22
+ end
23
+ spec.bindir = "exe"
24
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
+ spec.require_paths = ["lib"]
26
+
27
+ spec.add_dependency "dry-struct", "~> 0.5"
28
+
29
+ spec.add_development_dependency "bundler", "~> 1.16"
30
+ spec.add_development_dependency "rake", "~> 10.0"
31
+ spec.add_development_dependency "rspec", "~> 3.0"
32
+ spec.add_development_dependency "rt_rubocop_defaults", "~> 1.0"
33
+ spec.add_development_dependency "rubocop", "~> 0.58"
34
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "foxtracker/version"
4
+ require "foxtracker/application"
5
+
6
+ module Foxtracker
7
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-types"
4
+ require "dry-struct"
5
+
6
+ require "foxtracker/parser"
7
+
8
+ module Foxtracker
9
+ class Application
10
+ def initialize(argv)
11
+ @filename = argv.first
12
+ end
13
+
14
+ def run
15
+ xm = Foxtracker::Parser.read(@filename, debug: true)
16
+ require "pp"
17
+ pp xm
18
+ ensure
19
+ binding.irb
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-struct"
4
+
5
+ require "foxtracker/types"
6
+ require "foxtracker/format/extended_module/pattern"
7
+ require "foxtracker/format/extended_module/instrument"
8
+
9
+ module Foxtracker
10
+ module Format
11
+ class ExtendedModule < Dry::Struct
12
+ # Header
13
+ attribute :title, Types::Strict::String
14
+ attribute :tracker, Types::Strict::String
15
+ attribute :version_number, Types::Strict::Integer
16
+ attribute :header_size, Types::Strict::Integer
17
+ attribute :song_length, Types::Strict::Integer.constrained(min_size: 1, max_size: 256)
18
+ attribute :restart_position, Types::Strict::Integer
19
+ attribute :number_of_channels, Types::Strict::Integer
20
+ attribute :number_of_patterns, Types::Strict::Integer
21
+ attribute :number_of_instruments, Types::Strict::Integer
22
+ attribute :flags, Types::Strict::Integer
23
+ attribute :default_tempo, Types::Strict::Integer
24
+ attribute :default_bpm, Types::Strict::Integer
25
+ attribute :pattern_order, Types::Strict::Array.of(Types::Strict::Integer)
26
+
27
+ attribute :patterns, Types::Strict::Array.of(Pattern)
28
+ attribute :instruments, Types::Strict::Array.of(Instrument)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-struct"
4
+
5
+ require "foxtracker/types"
6
+ require "foxtracker/format/extended_module/sample"
7
+
8
+ module Foxtracker
9
+ module Format
10
+ class ExtendedModule < Dry::Struct
11
+ class Instrument < Dry::Struct
12
+ # first part -- these attributes MUST be set on an instrument
13
+ attribute :header_size, Types::Strict::Integer
14
+ attribute :name, Types::Strict::String
15
+ attribute :type, Types::Strict::Integer
16
+ attribute :number_of_samples, Types::Strict::Integer
17
+
18
+ # second part -- those are optional attributes
19
+ attribute :sample_header_size, Types::Strict::Integer.meta(omittable: true)
20
+ attribute :sample_keymap_assignments, Types::Strict::Array.of(Types::Strict::Integer)
21
+ .constrained(size: 96).meta(omittable: true)
22
+ # 48 bytes / 2 bytes = 24 words
23
+ attribute :volume_envelope, Types::Strict::Array.of(Types::Strict::Integer)
24
+ .constrained(size: 24).meta(omittable: true)
25
+ attribute :panning_envelope, Types::Strict::Array.of(Types::Strict::Integer)
26
+ .constrained(size: 24).meta(omittable: true)
27
+
28
+ # envelope points
29
+ attribute :number_of_volume_points, Types::Strict::Integer.meta(omittable: true)
30
+ attribute :number_of_panning_points, Types::Strict::Integer.meta(omittable: true)
31
+ attribute :volume_sustain_point, Types::Strict::Integer.meta(omittable: true)
32
+ attribute :volume_loop_start_point, Types::Strict::Integer.meta(omittable: true)
33
+ attribute :volume_loop_end_point, Types::Strict::Integer.meta(omittable: true)
34
+ attribute :panning_sustain_point, Types::Strict::Integer.meta(omittable: true)
35
+ attribute :panning_loop_start_point, Types::Strict::Integer.meta(omittable: true)
36
+ attribute :panning_loop_end_point, Types::Strict::Integer.meta(omittable: true)
37
+ attribute :volume_type, Types::Strict::String
38
+ .enum("on" => 0, "sustain" => 1, "loop" => 2, "sustain_and_loop" => 3)
39
+ .meta(omittable: true)
40
+ attribute :panning_type, Types::Strict::String
41
+ .enum("on" => 0, "sustain" => 1, "loop" => 2, "sustain_and_loop" => 3)
42
+ .meta(omittable: true)
43
+ attribute :vibrato_type, Types::Strict::Integer.meta(omittable: true)
44
+ attribute :vibrato_sweep, Types::Strict::Integer.meta(omittable: true)
45
+ attribute :vibrato_depth, Types::Strict::Integer.meta(omittable: true)
46
+ attribute :vibrato_rate, Types::Strict::Integer.meta(omittable: true)
47
+ attribute :volume_fadeout, Types::Strict::Integer.meta(omittable: true)
48
+ attribute :reserved, Types::Strict::Array.of(Types::Strict::Integer).meta(omittable: true)
49
+
50
+ attribute :samples, Types::Strict::Array.of(Sample).meta(omittable: true)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-struct"
4
+
5
+ require "foxtracker/types"
6
+
7
+ module Foxtracker
8
+ module Format
9
+ class ExtendedModule < Dry::Struct
10
+ class Note < Dry::Struct
11
+ attribute :note, Types::Strict::Integer
12
+ attribute :instrument, Types::Strict::Integer
13
+ attribute :volume, Types::Strict::Integer
14
+ attribute :effect_type, Types::Strict::Integer
15
+ attribute :effect_param, Types::Strict::Integer
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-struct"
4
+
5
+ require "foxtracker/types"
6
+ require "foxtracker/format/extended_module/note"
7
+
8
+ module Foxtracker
9
+ module Format
10
+ class ExtendedModule < Dry::Struct
11
+ class Pattern < Dry::Struct
12
+ attribute :header_size, Types::Strict::Integer
13
+ attribute :packing_type, Types::Strict::Integer
14
+ attribute :number_of_rows, Types::Strict::Integer.constrained(min_size: 1, max_size: 256)
15
+ attribute :packed_size, Types::Strict::Integer
16
+
17
+ attribute :channels, Types::Strict::Array.of(Types::Strict::Array.of(Note))
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-struct"
4
+
5
+ require "foxtracker/types"
6
+
7
+ module Foxtracker
8
+ module Format
9
+ class ExtendedModule < Dry::Struct
10
+ class Sample < Dry::Struct
11
+ attribute :sample_length, Types::Strict::Integer
12
+ attribute :sample_loop_start, Types::Strict::Integer
13
+ attribute :sample_loop_length, Types::Strict::Integer
14
+
15
+ attribute :volume, Types::Strict::Integer
16
+ attribute :finetune, Types::Strict::Integer
17
+ attribute :type, Types::Strict::Integer
18
+ attribute :panning, Types::Strict::Integer
19
+ attribute :relative_note_number, Types::Strict::Integer
20
+ attribute :packing_type, Types::Strict::Integer # Types::Strict::String.enum("delta" => 0, "adpcm" => 0xAD)
21
+ attribute :name, Types::Strict::String
22
+
23
+ # attribute :raw_data, Types::Strict::String
24
+ attribute :data, Types::Strict::Array.of(Types::Strict::Integer)
25
+
26
+ # returns the sample looping type (:none, :forward, or :pingpong)
27
+ def looping_type
28
+ ExtendedModule::Helpers.sample_looping_type(type)
29
+ end
30
+
31
+ # returns the sample type (8 bit or 16 bit)
32
+ def sample_type
33
+ ExtendedModule::Helpers.sample_type(type)
34
+ end
35
+
36
+ # returns if this sample is to be looped
37
+ def looping?
38
+ !sample_loop_length.zero?
39
+ end
40
+
41
+ # HACK: to remove :data from inspect output as sample data is really too spammy
42
+ # https://github.com/dry-rb/dry-struct/blob/cb41a5a03/lib/dry/struct.rb#L178
43
+ def inspect
44
+ klass = self.class
45
+ attrs = klass
46
+ .attribute_names
47
+ .reject { |key| key == :data }
48
+ .map { |key| " #{key}=#{@attributes[key].inspect}" }
49
+ .join
50
+ "#<#{klass.name || klass.inspect}#{attrs}>"
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "foxtracker/parser/extended_module"
4
+
5
+ module Foxtracker
6
+ module Parser
7
+ module_function
8
+
9
+ def read(filename, debug: false)
10
+ parse IO.binread(filename), debug: debug
11
+ end
12
+
13
+ def parse(bin, debug: false)
14
+ ExtendedModule.parse(bin, debug: debug)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtracker
4
+ module Parser
5
+ class Base
6
+ def self.parse(*args)
7
+ new.parse(*args)
8
+ end
9
+
10
+ def parse(*_args)
11
+ raise NotImplementedError
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "foxtracker/parser/base"
4
+ require "foxtracker/parser/support/extended_module"
5
+ require "foxtracker/format/extended_module"
6
+
7
+ module Foxtracker
8
+ module Parser
9
+ class ExtendedModule < Base
10
+ def parse(bin, debug: false)
11
+ bin = -bin
12
+ @debug = debug
13
+ args = {}
14
+ offset = 0
15
+
16
+ ##########
17
+ # header #
18
+ ##########
19
+ raise "not an XM module" unless bin[offset...(offset += 17)].casecmp("Extended module: ").zero?
20
+
21
+ args[:title] = bin[offset...(offset += 20)].rstrip
22
+ raise "invalid XM module" unless bin[offset...(offset += 1)] == "\x1A"
23
+
24
+ args[:tracker] = bin[offset...(offset += 20)].rstrip
25
+
26
+ args[:version_number] = bin[offset...(offset += 2)].unpack1("S<")
27
+
28
+ args[:header_size] = bin[offset...(offset += 4)].unpack1("L<")
29
+
30
+ %i[song_length restart_position number_of_channels number_of_patterns
31
+ number_of_instruments flags default_tempo default_bpm].each do |attr|
32
+ args[attr] = bin[offset...(offset += 2)].unpack1("S<")
33
+ end
34
+
35
+ args[:pattern_order] = bin[offset...(offset += args[:song_length])].unpack("C*")
36
+
37
+ if @debug
38
+ puts "song #{args[:title].inspect}"
39
+ puts "tracker #{args[:tracker].inspect} (#{format('%#06x', args[:version_number])})"
40
+ puts "-" * 25
41
+ end
42
+
43
+ ############
44
+ # patterns #
45
+ ############
46
+ offset, args[:patterns] = parse_patterns(bin, args)
47
+
48
+ ###############
49
+ # instruments #
50
+ ###############
51
+ offset, args[:instruments] = parse_instruments(bin, args, offset)
52
+
53
+ Format::ExtendedModule.new(args)
54
+ end
55
+
56
+ private
57
+
58
+ def parse_patterns(bin, xm_args)
59
+ patterns = []
60
+ offset = xm_args[:header_size] + 60
61
+
62
+ xm_args[:number_of_patterns].times do |pattern_no|
63
+ patterns << {}.tap do |pattern_args|
64
+ pattern_args[:header_size] = bin[offset...(offset += 4)].unpack1("L<")
65
+ pattern_args[:packing_type] = bin[offset...(offset += 1)].unpack1("C")
66
+ pattern_args[:number_of_rows] = bin[offset...(offset += 2)].unpack1("S<")
67
+ pattern_args[:packed_size] = bin[offset...(offset += 2)].unpack1("S<")
68
+ puts "pattern #{pattern_no.to_s(16)}" if @debug
69
+ pattern_args[:channels] = parse_pattern(
70
+ bin[offset...(offset += pattern_args[:packed_size])], xm_args, pattern_args
71
+ )
72
+ # offset += pattern_args[:packed_size] # skip for now
73
+ end
74
+ end
75
+
76
+ [offset, patterns]
77
+ end
78
+
79
+ def parse_pattern(bin, xm_args, pattern_args)
80
+ pattern = Array.new(xm_args[:number_of_channels])
81
+ offset = 0
82
+ pattern_args[:number_of_rows].times do
83
+ xm_args[:number_of_channels].times do |chan|
84
+ pattern[chan] ||= []
85
+ first_byte = bin[offset...(offset += 1)].unpack1("C")
86
+ note = { note: 0, instrument: 0, volume: 0, effect_type: 0, effect_param: 0 }
87
+ if first_byte & 128 == 128 # packed note
88
+ note[:note] = bin[offset...(offset += 1)].unpack1("C") if first_byte & 1 == 1
89
+ note[:instrument] = bin[offset...(offset += 1)].unpack1("C") if first_byte & 2 == 2
90
+ note[:volume] = bin[offset...(offset += 1)].unpack1("C") if first_byte & 4 == 4
91
+ note[:effect_type] = bin[offset...(offset += 1)].unpack1("C") if first_byte & 8 == 8
92
+ note[:effect_param] = bin[offset...(offset += 1)].unpack1("C") if first_byte & 16 == 16
93
+ pattern[chan] << note
94
+ next
95
+ end
96
+ note[:note] = first_byte
97
+ note[:instrument] = bin[offset...(offset += 1)].unpack1("C")
98
+ note[:volume] = bin[offset...(offset += 1)].unpack1("C")
99
+ note[:effect_type] = bin[offset...(offset += 1)].unpack1("C")
100
+ note[:effect_param] = bin[offset...(offset += 1)].unpack1("C")
101
+ pattern[chan] << note
102
+ end
103
+ end
104
+ puts pattern.transpose.map { |chan| chan.map { |note| format("%02d %2x %2x %2x %2x", note[:note], note[:instrument], note[:volume], note[:effect_type], note[:effect_param]) }.join(" | ") }.join("\n") if @debug
105
+ pattern
106
+ end
107
+
108
+ def parse_instruments(bin, xm_args, offset)
109
+ instruments = []
110
+
111
+ xm_args[:number_of_instruments].times do
112
+ instruments << {}.tap do |instrument_args|
113
+ # 1st part
114
+ instrument_args[:header_size] = bin[offset...(offset += 4)].unpack1("L<")
115
+ instrument_args[:name] = bin[offset...(offset += 22)].rstrip
116
+ instrument_args[:type] = bin[offset...(offset += 1)].unpack1("C")
117
+ instrument_args[:number_of_samples] = bin[offset...(offset += 2)].unpack1("S<")
118
+
119
+ if @debug
120
+ puts "instrument #{instrument_args[:name].inspect}:"
121
+ puts "- header_size: #{format('%#06x', instrument_args[:header_size])}"
122
+ puts "- number_of_samples: #{instrument_args[:number_of_samples]}"
123
+ end
124
+ if instrument_args[:number_of_samples].zero?
125
+ offset -= 33 # realignment hack from xmp
126
+ next
127
+ end
128
+
129
+ # 2nd part
130
+ instrument_args[:sample_header_size] = bin[offset...(offset += 4)].unpack1("L<")
131
+ puts "- sample_header_size: #{format('%#06x', instrument_args[:sample_header_size])}" if @debug
132
+ instrument_args[:sample_keymap_assignments] = bin[offset...(offset += 96)].unpack("C*")
133
+ instrument_args[:volume_envelope] = bin[offset...(offset += 48)].unpack("S<*")
134
+ instrument_args[:panning_envelope] = bin[offset...(offset += 48)].unpack("S<*")
135
+
136
+ %i[number_of_volume_points number_of_panning_points
137
+ volume_sustain_point volume_loop_start_point volume_loop_end_point
138
+ panning_sustain_point panning_loop_start_point panning_loop_end_point
139
+ volume_type panning_type
140
+ vibrato_type vibrato_sweep vibrato_depth vibrato_rate].each do |attr|
141
+ instrument_args[attr] = bin[offset...(offset += 1)].unpack1("C")
142
+ if @debug
143
+ print "- #{attr}: #{format('%#04x', instrument_args[attr])} -- "
144
+ p instrument_args[attr]
145
+ end
146
+ end
147
+
148
+ instrument_args[:volume_fadeout] = bin[offset...(offset += 2)].unpack1("S<")
149
+ instrument_args[:reserved] = bin[offset...(offset += 22)].unpack("S<*")
150
+ puts "- volume_fadeout: #{format('%#06x', instrument_args[:volume_fadeout])}" if @debug
151
+
152
+ offset, instrument_args[:samples] = parse_samples(bin, instrument_args, offset)
153
+ end
154
+ end
155
+
156
+ [offset, instruments]
157
+ end
158
+
159
+ def parse_samples(bin, instrument_args, offset)
160
+ samples = []
161
+
162
+ # first the sample headers ...
163
+ puts "- samples:" if @debug
164
+ instrument_args[:number_of_samples].times do
165
+ samples << {}.tap do |sample_args|
166
+ start_offset = offset
167
+ sample_args[:sample_length] = bin[offset...(offset += 4)].unpack1("L<")
168
+ sample_args[:sample_loop_start] = bin[offset...(offset += 4)].unpack1("L<")
169
+ sample_args[:sample_loop_length] = bin[offset...(offset += 4)].unpack1("L<")
170
+
171
+ sample_args[:volume] = bin[offset...(offset += 1)].unpack1("C")
172
+ sample_args[:finetune] = bin[offset...(offset += 1)].unpack1("c")
173
+ sample_args[:type] = bin[offset...(offset += 1)].unpack1("C")
174
+ sample_args[:panning] = bin[offset...(offset += 1)].unpack1("C")
175
+ sample_args[:relative_note_number] = bin[offset...(offset += 1)].unpack1("c")
176
+ sample_args[:packing_type] = bin[offset...(offset += 1)].unpack1("C")
177
+
178
+ sample_args[:name] = bin[offset...(offset += 22)].rstrip
179
+ if @debug
180
+ puts " - name: #{sample_args[:name].inspect}"
181
+ puts " type: #{Support::ExtendedModule.sample_type(sample_args[:type])}bit"
182
+ puts " length: #{format('%#06x', sample_args[:sample_length])}"
183
+ puts " loop_start: #{format('%#06x', sample_args[:sample_loop_start])}"
184
+ puts " loop_length: #{format('%#06x', sample_args[:sample_loop_length])}"
185
+ puts " volume: #{format('%#04x', sample_args[:volume])}"
186
+ puts " finetune: #{format('%#04x', sample_args[:finetune])}"
187
+ puts " panning: #{format('%#04x', sample_args[:panning])}"
188
+ end
189
+ diff = instrument_args[:sample_header_size] - (offset - start_offset)
190
+ puts " offset diff from instrument header: #{diff}" if @debug
191
+ raise unless diff.zero?
192
+ end
193
+ end
194
+
195
+ # ... then the sample data!
196
+ samples.each do |sample_args|
197
+ sample_args[:raw_data] = bin[offset...(offset += sample_args[:sample_length])]
198
+ sample_args[:data] = unpack_sample_data(sample_args)
199
+ end
200
+
201
+ [offset, samples]
202
+ end
203
+
204
+ def unpack_sample_data(sample_args)
205
+ puts "unpacking sample #{sample_args[:name].inspect} ..." if @debug
206
+ unpack_str = case Support::ExtendedModule.sample_type(sample_args[:type])
207
+ when 8 then "c*"
208
+ when 16 then "s<*"
209
+ else
210
+ raise "this should never happen(tm)"
211
+ end
212
+ packed_samples = sample_args.delete(:raw_data).unpack(unpack_str)
213
+
214
+ raise "ADPCM samples are not supported yet" if sample_args[:packing_type] == 0xAD
215
+
216
+ # delta compression
217
+ [].tap do |samples|
218
+ previous_sample = 0
219
+ packed_samples.each do |packed_sample|
220
+ samples << (previous_sample += packed_sample)
221
+ end
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtracker
4
+ module Parser
5
+ module Support
6
+ module ExtendedModule
7
+ module_function
8
+
9
+ # returns the sample looping type (:none, :forward, or :pingpong)
10
+ def sample_looping_type(sample_type_byte)
11
+ case sample_type_byte & 0b11
12
+ when 0 then :none
13
+ when 1 then :forward
14
+ when 2 then :pingpong
15
+ else
16
+ raise "this should never happen(tm)"
17
+ end
18
+ end
19
+
20
+ # returns the sample type (8 bit or 16 bit)
21
+ def sample_type(sample_type_byte)
22
+ ((sample_type_byte & 0b1000) >> 3).zero? ? 8 : 16
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-types"
4
+
5
+ module Foxtracker
6
+ class Types
7
+ include Dry::Types.module
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Foxtracker
4
+ VERSION = "0.1.0.pre1337"
5
+ end
metadata ADDED
@@ -0,0 +1,158 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: foxtracker
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0.pre1337
5
+ platform: ruby
6
+ authors:
7
+ - Georg Gadinger
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-08-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dry-struct
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.16'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.16'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rt_rubocop_defaults
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.58'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.58'
97
+ description: Foxtracker is a parser for tracker music formats. Right now it only
98
+ supports XM (FastTracker II) modules. Support for more formats is to be done.
99
+ email:
100
+ - nilsding@nilsding.org
101
+ executables:
102
+ - foxtracker
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - ".gitignore"
107
+ - ".rspec"
108
+ - ".rubocop.yml"
109
+ - ".ruby-version"
110
+ - ".travis.yml"
111
+ - CODE_OF_CONDUCT.md
112
+ - Gemfile
113
+ - Gemfile.lock
114
+ - LICENSE.txt
115
+ - README.md
116
+ - Rakefile
117
+ - bin/console
118
+ - bin/setup
119
+ - exe/foxtracker
120
+ - foxtracker.gemspec
121
+ - lib/foxtracker.rb
122
+ - lib/foxtracker/application.rb
123
+ - lib/foxtracker/format/extended_module.rb
124
+ - lib/foxtracker/format/extended_module/instrument.rb
125
+ - lib/foxtracker/format/extended_module/note.rb
126
+ - lib/foxtracker/format/extended_module/pattern.rb
127
+ - lib/foxtracker/format/extended_module/sample.rb
128
+ - lib/foxtracker/parser.rb
129
+ - lib/foxtracker/parser/base.rb
130
+ - lib/foxtracker/parser/extended_module.rb
131
+ - lib/foxtracker/parser/support/extended_module.rb
132
+ - lib/foxtracker/types.rb
133
+ - lib/foxtracker/version.rb
134
+ homepage: https://github.com/nilsding/foxtracker
135
+ licenses:
136
+ - MIT
137
+ metadata: {}
138
+ post_install_message:
139
+ rdoc_options: []
140
+ require_paths:
141
+ - lib
142
+ required_ruby_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ required_rubygems_version: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">"
150
+ - !ruby/object:Gem::Version
151
+ version: 1.3.1
152
+ requirements: []
153
+ rubyforge_project:
154
+ rubygems_version: 2.7.6
155
+ signing_key:
156
+ specification_version: 4
157
+ summary: a parser for tracker music formats
158
+ test_files: []