Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
Matt Allan committed Dec 26, 2018
0 parents commit aa8601c
Show file tree
Hide file tree
Showing 10 changed files with 583 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
com.matta.exs24.xrnx
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# EXS24 For Renoise

This is a renoise tool that adds support for the Logic EXS24 sample format.

## Installation

## Usage

Once the tool is installed .exs instruments should be visible in the sidebar. Clicking on the instrument will load it just like a native renoise instrument.

The tool will attempt to load the samples automatically. If the sample path cannot be determined you will be prompted to select the folder.

This tool can currently map the following properties for zones and samples:

- name
- fine tune
- panning
- oneshot
- base note
- note range
- velocity range
- loop start
- loop end
- loop reverse

## Packaging

```
$ zip -vr com.matta.exs24.xrnx * -x@./exclude.list
```
5 changes: 5 additions & 0 deletions exclude.list
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
*.svn*
*.DS_Store*
*.git*
test*
exclude.list
189 changes: 189 additions & 0 deletions exs24.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
require "util/binary"
require "util/str"

function load_exs(path)
local fh = io.open(path, "rb")
if fh == nil then
return false
end

fh:seek("set", 16)
local magic = fh:read(4)

if magic ~= "SOBT" and magic ~= "SOBJ" and magic ~= "TBOS" and magic ~= "JBOS" then
return false
end

local big_endian = false
if magic == "SOBT" or magic == "SOBJ" then
big_endian = true
end

local is_size_expanded = false
fh:seek("set", 4)
local header_size = read_dword(fh, big_endian)
if header_size > 0x8000 then
is_size_expanded = true
end

local exs = {zones = {}, samples = {}}
local i = 0
local data_size = fh:seek("end")

while (i + 84 < data_size) do
fh:seek("set", i)
local sig = read_dword(fh, big_endian)

fh:seek("set", i + 4)
local size = read_dword(fh, big_endian)

fh:seek("set", i + 16)
local magic = fh:read(4)

if is_size_expanded and size > 0x8000 then
size = size - 0x8000
end

local chunk_type = bit.rshift(bit.band(sig, 0x0F000000), 24)

if chunk_type == 0x01 then
if size < 104 then
return false
end
table.insert(exs.zones, create_zone(fh, i, size + 84, big_endian))
elseif chunk_type == 0x03 then
if size ~= 336 and size ~= 592 then
return false
end
table.insert(exs.samples, create_sample(fh, i, size + 84, big_endian))
end
i = i + size + 84
end

return exs
end

function create_zone(fh, i, size, big_endian)
local zone = {}

fh:seek("set", i + 8)
zone.id = read_dword(fh, big_endian)

fh:seek("set", i + 20)
zone.name = rtrim(fh:read(64))

fh:seek("set", i + 84)
local zone_opts = string.byte(fh:read(1))
zone.pitch = bit.band(zone_opts, bit.lshift(1, 1)) == 0
zone.oneshot = bit.band(zone_opts, bit.lshift(1, 0)) ~= 0
zone.reverse = bit.band(zone_opts, bit.lshift(1, 2)) ~= 0

fh:seek("set", i + 85)
zone.key = string.byte(fh:read(1))

fh:seek("set", i + 86)
zone.fine_tuning = twos_complement(string.byte(fh:read(1)), 8)

fh:seek("set", i + 87)
zone.pan = twos_complement(string.byte(fh:read(1)), 8)

fh:seek("set", i + 88)
zone.volume = twos_complement(string.byte(fh:read(1)), 8)
fh:seek("set", i + 164)
zone.coarse_tuning = twos_complement(string.byte(fh:read(1)), 8)

fh:seek("set", i + 90)
zone.key_low = string.byte(fh:read(1))

fh:seek("set", i + 91)
zone.key_high = string.byte(fh:read(1))

zone.velocity_range_on = bit.band(zone_opts, bit.lshift(1, 3)) ~= 0

fh:seek("set", i + 93)
zone.velocity_low = string.byte(fh:read(1))

fh:seek("set", i + 94)
zone.velocity_high = string.byte(fh:read(1))

fh:seek("set", i + 96)
zone.sample_start = read_dword(fh, big_endian)

fh:seek("set", i + 100)
zone.sample_end = read_dword(fh, big_endian)

fh:seek("set", i + 104)
zone.loop_start = read_dword(fh, big_endian)

fh:seek("set", i + 108)
zone.loop_end = read_dword(fh, big_endian)

fh:seek("set", i + 112)
zone.loop_crossfade = read_dword(fh, big_endian)

fh:seek("set", i + 117)
local loop_opts = string.byte(fh:read(1))
zone.loop_on = bit.band(loop_opts, bit.lshift(1, 0)) ~= 0
zone.loop_equal_power = bit.band(loop_opts, bit.lshift(1, 1)) ~= 0

if bit.band(zone_opts, bit.lshift(1, 6)) == 0 then
zone.output = -1
else
fh:seek("set", i + 166)
zone.output = string.byte(fh:read(1))
end

fh:seek("set", i + 172)
zone.group_index = read_dword(fh, big_endian)

fh:seek("set", i + 176)
zone.sample_index = read_dword(fh, big_endian)

zone.sample_fade = 0
if size > 188 then
fh:seek("set", i + 188)
zone.sample_fade = read_dword(fh, big_endian)
end

zone.offset = 0
if size > 192 then
fh:seek("set", i + 192)
zone.offset = read_dword(fh, big_endian)
end
return zone
end

function create_sample(fh, i, size, big_endian)
local sample = {}

fh:seek("set", i + 8)
sample.id = read_dword(fh, big_endian)

fh:seek("set", i + 20)
sample.name = rtrim(fh:read(64))

fh:seek("set", i + 88)
sample.length = read_dword(fh, big_endian)

fh:seek("set", i + 92)
sample.sample_rate = read_dword(fh, big_endian)

fh:seek("set", i + 96)
sample.bit_depth = string.byte(fh:read(1))

fh:seek("set", i + 112)
sample.type = read_dword(fh, big_endian)

fh:seek("set", i + 164)
sample.file_path = rtrim(fh:read(256))

if size > 420 then
fh:seek("set", i + 420)
sample.file_name = rtrim(fh:read(256))
else
fh:seek("set", i + 20)
sample.file_name = rtrim(fh:read(64))
end

return sample
end
146 changes: 146 additions & 0 deletions main.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
require "exs24"
require "util/process_slicer"

local function sample_path(instrument_path, instrument_filename, sample_filename)
-- Samples in the current path
if io.exists(instrument_path .. sample_filename) then
return instrument_path
end

-- GarageBand instrument
-- i.e. "Sampler/Sampler Instruments/Puremagnetik/Eight Bit/" ->
-- "Sampler/Sampler Files/Puremagnetik Samples/Eight Bit"
if instrument_path:find("Sampler Instruments") then
local garageband_path = instrument_path:gsub(
"(%w+)/Sampler Instruments/(%w+)/(%w+)",
"%1/Sampler Files/%2 Samples/%3"
)
if io.exists(garageband_path .. sample_filename) then
return garageband_path
end
end

-- Logic instrument
-- i.e. "Logic/Sampler Instruments/Puremagnetik/alphaSynth" ->
-- "GarageBand/Instrument Library/Sampler/Sampler Files/Puremagnetik Samples/alphaSynth"
if instrument_path:find("Logic") then
local logic_path = instrument_path:gsub(
"(%w*)Logic/Sampler Instruments/(%w+)/(%w+)",
"%1/GarageBand/Instrument Library/Sampler/Sampler Files/%2 Samples/%3"
)
if io.exists(logic_path .. sample_filename) then
return logic_path
end
end

-- Sample From Mars
-- i.e. "DX100 From Mars/Logic EXS/DX100 From Mars/Leads/Box Cello.exs" ->
-- "DX100 From Mars/WAV/Box Cello"
if instrument_path:find("Logic EXS") then
local mars_path = instrument_path:gsub(
"(%w+)/Logic EXS/.+",
"%1/WAV/"
) .. instrument_filename:gsub("(.+)\.exs", "%1/")
if io.exists(mars_path .. sample_filename) then
return mars_path
end
end

return renoise.app():prompt_for_path("Sample files for " .. instrument_filename .. ":")
end

local function import_samples(instrument, exs, sample_path)
local missing_samples = 0

for k,zone in pairs(exs.zones) do
if exs.samples[zone.sample_index + 1] then
local exs_sample = exs.samples[zone.sample_index + 1]

if io.exists(sample_path .. exs_sample.file_name) then
local sample = instrument:insert_sample_at(#instrument.samples + 1)
if sample.sample_buffer:load_from(sample_path .. exs_sample.file_name) == true then
sample.name = exs_sample.name
-- todo: volume must be 0 - 4, what range is the exs using?
-- exs is using a twos complement byte so it has to be within -128 -127?
sample.volume = 1
sample.fine_tune = math.max(math.min(zone.fine_tuning, 127), -127)
sample.panning = math.max(math.min((zone.pan / 200) + .5, 1.0), 0.0)
sample.oneshot = zone.oneshot
sample.sample_mapping.base_note = math.max(math.min(zone.key, 119), 0)
sample.sample_mapping.note_range = {
math.max(math.min(zone.key_low, 119), 0),
math.min(119, zone.key_high)
}
sample.sample_mapping.velocity_range = {zone.velocity_low, zone.velocity_high}
sample.loop_start = zone.loop_start + 1
sample.loop_end = zone.loop_end - 1
if zone.reverse then
sample.loop_mode = sample.LOOP_MODE_REVERSE
end
end
else
missing_samples = missing_samples + 1
end
else
missing_samples = missing_samples + 1
end
renoise.app():show_status(string.format("Importing EXS24 instrument (%d%%)...",((k/#exs.zones))*100))
coroutine.yield()
end

renoise.app():show_status("Importing Logic EXS24 instrument complete")
if missing_samples ~= 0 then
renoise.app():show_warning(string.format("%d samples could not be found", missing_samples))
end
end

local function import_exs(path)
renoise.app():show_status("Importing EXS24 instrument...")

local exs = load_exs(path)

if exs == false then
renoise.app():show_error("The EXS24 instrument could not be loaded")
table.clear(exs)
return false
end

if #exs.zones == 0 then
renoise.app():show_status("The EXS24 instrument did not contain any zones")
table.clear(exs)
return true
end

if #exs.samples == 0 then
renoise.app():show_status("The EXS24 instrument did not contain any samples")
table.clear(exs)
return true
end

local last_slash_pos = path:match"^.*()/"
local instrument_filename = path:sub(last_slash_pos + 1)
local instrument_path = path:sub(1, last_slash_pos)

local instrument = renoise.song().selected_instrument
instrument:clear()
instrument.name = instrument_filename:sub(1, -5)

local sample_path = sample_path(instrument_path, instrument_filename, exs.samples[1].file_name)

if not sample_path then
renoise.app():show_error("The EXS24 sample path could not be found")
end

local process = ProcessSlicer(import_samples, instrument, exs, sample_path)
process:start()

return true
end

if renoise.tool():has_file_import_hook("instrument", {"exs"}) == false then
renoise.tool():add_file_import_hook({
category = "instrument",
extensions = {"exs"},
invoke = import_exs
})
end
11 changes: 11 additions & 0 deletions manifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<RenoiseScriptingTool doc_version="0">
<ApiVersion>5</ApiVersion>
<Id>com.matta.exs24</Id>
<Version>0.1</Version>
<Author>Matt Allan [[email protected]]</Author>
<Name>EXS24</Name>
<Category>Instruments</Category>
<Description>A tool that allows using EXS24 Instruments</Description>
<Homepage>http://tools.renoise.com/tools/exs24</Homepage>
</RenoiseScriptingTool>
Loading

0 comments on commit aa8601c

Please sign in to comment.