Skip to main content

Simple object-oriented SFZ parsing and manipulation.

Project description

sfzen

Simple object-oriented SFZ parsing and manipulation.

Credits

This project borrows a good deal from sfzlint by J. Isaac Stone, particularly the lark parser definition. I made everything as object-oriented and pythonic as I could, and added a few functions which sfzlint didn't cover. Many thanks to "jisaacstone" for doing the ground work. Anyone who contributes to the Linux music community deserves recognition!

Basic Usage

from sfzen import SFZ

sfz = SFZ(filename)
... do stuff ...
sfz.write(sys.stdout)

SFZ structure and navigation.

The structure of an instance of SFZ follows the SFZ format. It may have headers of various types, and headers can contain opcodes.

The SFZ class exposes a "subheaders" property which returns a list of headers which are immediate children of the SFZ. Each subheader also has a "subheaders" property which returns a list of headers contained in it.

You could traverse the SFZ structure by iterating via the the "subheaders()" method. Another way is to call the "walk()" method. This is a generator function which recuses through the tree structure of the SFZ, yielding an element (header or opcode) on each iteration. The return value of "walk()" is a tuple of (element, depth).

For example:

from sfzen import SFZ
sfz = SFZ(filename)
for elem, depth in sfz.walk():
	if isinstance(elem, Header):
		print("  " * depth, elem)

The above example is redundant, however, as the "dump()" method does just this.

Opcodes

Opcodes are accessed using the "opcodes" property, which returns a dictionary. The keys are the opcode names, and values are instances of the Opcode class. (Or Sample class, which is described below.)

Note: The root SFZ object may not contain opcodes.

You can traverse up the hierarchy using the "parent" property of both Opcode and Header.

Sample opcodes

The "<sample>" opcode is pretty important in an SFZ file. The sample value points to a location in the filesystem, so filesystem -related methods would be needed. Therefore, there is a dedicated Sample class which extends the Opcode class and adds new functions.

One useful property of the Sample class is "abspath". Sample values are usually defined relative to the root path of the SFZ. This function resolves that relative value into an absolute path pointing to the sample in the filesystem.

Regions

Samples are defined inside a tag in an SFZ. So the "parent" property of a sample will (probably) be a Region. This method of traversal is quite useful:

for sample in sfz.samples():
	region = sample.parent
	# Get all the opcodes in the region containing the sample:
	opcodes = region.opcodes

Often, a <region> is contained inside a <group> which contains opcodes which might affect the sound of that sample. In order to retrieve all the opcodes which will affect the sound of the sample, use the "inherited_opcodes()" method of its parent Region. This will return all the opcodes for the Region, as well as any which are defined in a Header above that Region in the SFZ structure.

for sample in sfz.samples():
	region = sample.parent
	# Get all the opcodes in the region containing the sample, as well
	# as opcodes from parent Headers which will affect that sample:
	all_opcodes = region.inherited_opcodes()

Individual opcodes can be retrieved using the "opcode(<opcode_name>)" method. This method retrieves an Opcode which is contained in the Header on which it is called:

for sample in sfz.samples():
	region = sample.parent
	loopmode = region.opcode("loopmode")

If "loopmode" is not defined in the Region, but is defined in a parent header, this method will not find it. Like the "inherited_opcodes()" method described above, the "iopcode()" method will retrive an opcode defined in a parent header.

So, to retrieve the loopmode which affects the sample, regardless of where it is defined, you will do:

loopmode = region.iopcode("loopmode")

Header attributes

It gets better.

The "opcode()" and "iopcode()" methods return Opcode objects, which are easy enough to manipulate using their "value" property. But you can avoid that by just referencing the opcode name as an attribute of the Header.

So, for example, if a contains an opcode named "loopmode", you can retrieve it's value by using "region.loopmode".

for sample in sfz.samples():
	region = sample.parent
	loopmode = region.loopmode

Just as with the iopcode function, if there is no Opocode named "loopmode" in the Region, the attribute lookup will move up the hierarchy to the parent Header of that Region, until it finds one. If nothing is found, the value will be None.

Aliases

Note that there are two opcodes in the SFZ format which define the way a sample is looped, "loopmode" and "loop_mode". You can use either one, as you prefer. Using attribute access, opcode aliases are also checked. So if you access the "loopmode" property of a Region that has no Opocode named "loopmode", but does contain an Opcode named "loop_mode", the value of the Opcode named "loop_mode" will be returned for the "loopmode" attribute.

The following example should make this clear:

from sys import stdout
from sfzen import SFZ
from sfzen.sfz_elems import Group, Region

sfz = SFZ()
group = Group()
sfz.append_subheader(group)
region = Region()
group.append_subheader(region)
print(region.loopmode)
group.loopmode = "loop_continuous"
print(region.loopmode)
region.loopmode = "one_shot"
print(region.loopmode)

print()

sfz.write(stdout)

The output of the above script will be:

None
loop_continuous
one_shot

// ----------------------------------------------------------------------------
// None
// ----------------------------------------------------------------------------

<group>
loopmode=loop_continuous

<region>
loopmode=one_shot

Normalization and validation

An opcode named "offset_cc22" will not be literally defined in the spec, but is an instance of the "offset_ccN" opcode. To retrieve the opcode name defined in the spec, use the "normal_opcode()" function found in the sfz_elems module. Other functions in that module, like "opcode_definition()" and "validation_rule()" normalize the opcode name when doing a lookup. Aliases are normalized as well.

There is a lot more to write about regarding validation. Look at the source code or the help() text in the python interpreter to get an idea how it might be used. Creating an "sfz-validate" script is definitely on my TODO list. If anyone would like to bang one out... go for it!

Sample modes when saving

Normally, an SFZ file's sample paths are given as relative to the SFZ file. When saving an SFZ object, you have multiple choices as to how you want the samples saved and referenced. (This is particularly useful when copying an sfz.)

SAMPLES_ABSPATH

The file names are written as absolute paths.

SAMPLES_RESOLVE

The file names are written as paths relative to the original sfz.

SAMPLES_COPY

The file names are written as relative paths, and the sample files are copied to a "samples-<sfz_name>" subfolder.

SAMPLES_MOVE

The file names are written as relative paths, and the sample files are moved to a "samples-<sfz_name>" subfolder.

SAMPLES_SYMLINK

The file names are written as relative paths, and the sample files are symlinked in a "samples-<sfz_name>" subfolder.

SAMPLES_HARDLINK

The file names are written as relative paths, and the sample files are hard -linked in a "samples-<sfz_name>" subfolder. (Not available on the "windoze operating system".)

Simplification

After building up an SFZ, you can automatically create groups which define opcodes common to the regions they contain, using the "simplified()" method of the SFZ class. This creates a copy of the original SFZ, with some redundant opcodes removed and common opcodes grouped inside <group> and <global> headers.

There are two different grouping modes:

GLOBALIZE_UNIVERSAL

Only opcodes which are common to each and every region will be taken out of that region and put into a <global> header.

GLOBALIZE_NUMEROUS

Opocodes which are the same for at least half of the regions will be taken out of that region and put into a <global> header.

Note that these methods compare the inherited opcodes of the regions.

Using simplification, I saw significant space reductions on some .sfz files found in the public domain, as well as some generated from SoundFonts using Polyphone.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sfzen-1.8.1.tar.gz (84.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sfzen-1.8.1-py2.py3-none-any.whl (93.1 kB view details)

Uploaded Python 2Python 3

File details

Details for the file sfzen-1.8.1.tar.gz.

File metadata

  • Download URL: sfzen-1.8.1.tar.gz
  • Upload date:
  • Size: 84.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.3

File hashes

Hashes for sfzen-1.8.1.tar.gz
Algorithm Hash digest
SHA256 00cee65ce132a07d40d0d841648acb5c7328a7cc8c37145421717ee0c9ce42a1
MD5 4f80bd82608804fd5e349588f16845eb
BLAKE2b-256 6197b889371fc8c375cab74d952228cd8928ddd5ee2f43fdaee77cb5536f2f0f

See more details on using hashes here.

File details

Details for the file sfzen-1.8.1-py2.py3-none-any.whl.

File metadata

  • Download URL: sfzen-1.8.1-py2.py3-none-any.whl
  • Upload date:
  • Size: 93.1 kB
  • Tags: Python 2, Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.3

File hashes

Hashes for sfzen-1.8.1-py2.py3-none-any.whl
Algorithm Hash digest
SHA256 e4204a531aa87d4e59bc4b474e4e8a592155256188d985aa6b1c1d4a71592d05
MD5 79fbf0cfdaa27ed0058b3390530c3723
BLAKE2b-256 68248d72f8877d803feab73e15b06623f135380f80d9d6fc8b6ee703fb9af355

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page