A note data abstraction layer for exporting, but not reinventing the wheel

Pretty sure a lot of tools already implemented some note data abstraction, which ultimatively gives you:

  • Note data sorted by track and time
  • Note data contains duration, exact timestamp. Takes care of missing note-offs, disabled sequence slots etc.
  • Note data contains note data properly mapped, e.g. “velocity”, “panAmount”, etc.
  • Elimates pattern abstraction, linear output

EDIT: If you are interested in what I came up with here, please have a look into the code of my DawProject tool.

Here is an early approach, read comment:

--[[

    Note Abstraction Layer
    for linear DAW export

    Generates an event table of the whole linear song,
    sorted by track number and absolute line number.
    Such a note event also contains merged data as:

    - duration, exact timestamp. Takes care of missing note-offs, disabled sequence slots etc.
    - normalized per-note-data like “velocity”, "pan", etc.
    - Elimates pattern abstraction, linear output


]]


class "NoteAbstraction"

function NoteAbstraction:__init()
end

function NoteAbstraction:generateSongNoteEvents()
    local noteEvents = {}
    local activeNotes = {}
    local lastVelocities = {}
    local noteKey = nil
    local patternIndex = nil
    local patternLines = nil
    local seqIsMuted = {}
    local lineOffset = 0

    for seqIndex = 1, #Song.sequencer.pattern_sequence do
        patternIndex = Song.sequencer.pattern_sequence[seqIndex]
        patternLines = Song.patterns[patternIndex].number_of_lines

        for position, noteColumn in Song.pattern_iterator:note_columns_in_pattern(patternIndex) do
            noteKey = position.column .. "_" .. position.track

            local checkForNoteEvent = function(_noteKey)
                -- Note-Off / Cut
                local noteOn = activeNotes[_noteKey]
                if noteOn then
                    noteOn.lineDuration = (lineOffset + position.line - 1) - noteOn.absLineNum
                    noteOn.duration = ((lineOffset + position.line - 1) * 256 + noteColumn.delay_value) -
                        noteOn.timestamp
                    noteOn.releaseVelocity = noteColumn.volume_value < 128 and (noteColumn.volume_value / 127) or nil
                    table.insert(noteEvents, noteOn)
                    -- remove note-on
                    activeNotes[_noteKey] = nil
                end
            end

            if (position.line == 1 and position.column == 1) then
                local _seqIsMuted = Song.sequencer:track_sequence_slot_is_muted(position.track, seqIndex)
                if (seqIsMuted[noteKey] ~= _seqIsMuted) then
                    seqIsMuted[noteKey] = _seqIsMuted
                    checkForNoteEvent(noteKey)
                end
            end

            if noteColumn.is_empty then
                goto continue
            end



            if noteColumn.note_value >= 0 and noteColumn.note_value < renoise.PatternLine.NOTE_OFF then
                checkForNoteEvent(noteKey)

                -- add note-on
                activeNotes[noteKey] = {
                    key = noteColumn.note_value,
                    noteString = noteColumn.note_string,
                    velocity = noteColumn.volume_value < 128 and (noteColumn.volume_value / 127) or
                        lastVelocities[noteKey] or
                        1,
                    pan = noteColumn.panning_value < 128 and (noteColumn.panning_value / 127) or nil,
                    delay = noteColumn.delay_value,
                    seqNum = seqIndex,
                    patternNum = position.pattern,
                    patternDuration = patternLines * 256,
                    columnNum = position.column,
                    trackNum = position.track,
                    lineNum = position.line - 1,
                    absLineNum = lineOffset + position.line - 1,
                    timestamp = (lineOffset + position.line - 1) * 256 + noteColumn.delay_value,
                    patternRelTimestamp = (position.line - 1) * 256 + noteColumn.delay_value,
                    patternTimestamp = lineOffset * 256
                }
                if (noteColumn.volume_value < 128) then
                    lastVelocities[noteKey] = noteColumn.volume_value
                end
            elseif noteColumn.note_value == renoise.PatternLine.NOTE_OFF then
                checkForNoteEvent(noteKey)
            end

            ::continue::
        end
        lineOffset = lineOffset + patternLines
    end

    -- end playing notes
    for _, noteOn in pairs(activeNotes) do
        if noteOn then
            noteOn.lineDuration = lineOffset - noteOn.absLineNum
            noteOn.duration = (lineOffset * 256) - noteOn.timestamp
            table.insert(noteEvents, noteOn)
        end
    end

    table.sort(noteEvents, function(a, b)
        if (a.trackNum == b.trackNum) then
            return a.timestamp < b.timestamp
        end
        return a.trackNum < b.trackNum
    end)

    return noteEvents
end

Simply have a look at the code of related tools for inspiration? Most tools out there are open source. If you want MIDI, have a look at the MIDI export tool.

If that’s still too much effort, ask some LLM to write that for you. That’s how the kids do it now.

2 Likes

Ok, well LLM is very imprecise and most of the time gives a raw scaffolding, based on often not so good public internet knowledge… At least in my experience.

Maybe the kids are right though, and I just need to learn how to properly use the LLM, or use the paid versions instead…

I know joule already did that, and yes, will look into other tools. The design pattern in midi export tool seems to be a bit oldschool, I was looking for an abstraction which ultimatively is easy to use.

Currently trying to make some “note abstraction service” class or something.

Any hints for a modern tool, maybe I should have a look into Simple Pianoroll?

Rarely is it wise to begin from an abstraction. Abstractions necessarily arise from concrete examples. Examine what you’re trying to do, list all your requirement, and find the shortest path to satisfy them. I would not be over-worried about “reinventing the wheel”. Sometimes as programmers we need a slightly different wheel, and the easiest path is to invent it rather than reshape an existing one.

3 Likes

Yeah, if 100% optimization isn’t needed, this can be made quite quickly using renoise.PatternIterator already. Using ripairs, keeping a little track of lengths and storing the data any way you want.

Don’t agree, Renoise pattern, track, line logic is very iterative…

If I do not build a proper abstraction first, details mentioned in first post, I will end up will extremely over-complicated logic, just to stay in the vanilla Renoise logic. So it is wise to build an abstraction layer first. You should test and debug the layer first.

But this is on my point of view, please read further here:

1 Like

In post #1 is the current abstraction layer class.

Actually did you test copilot in VSCode once? I never so far… I doubt that it will give you nice results specifically for Renoise LUA api, but maybe I am wrong. I am a bit overhelmed by VSCode configuration, usually working in PHPStorm and other JetBrains IDEs :laughing: Those are slow, but very comfortable.

Currently some weird AI is changing my code on the fly in my Renoise project while coding, mostly adding nasty bugs, giving stupid suggestions or removing parts of lines. Really weird. No idea what AI that is, maybe Musks, just here to destroy. Can’t be copilot, because it seems you have to enter login data for that first. (I bet it’s just two code formatters running at once or so)

does anyone have a tl;dr of what ffx is trying to accomplish?

a real-life usecase would help.

“i want to <do this> and in order to do it, i need to <have this>

I believe he’s been trying to convert xrns files into DAWproject+Redux files. So he wanted a song representation that would make that easier. I think he’s worked it out.

You are certainly right. I was writing here just about my personal expectations, it’s not that I don’t love the Renoise API - which other DAW has such a complete API at all? Recently Steinberg added some JS API for controllers, and also Bitwig still is on the way (where Taktik was 2010 or so already), via two APIs, but none of these is nowhere near the feature set of the Renoise API.

If I am “criticizing” here, it is mostly about my expection or inability to properly code an everlasting tool (which I still can maintain 10 years later easily) without an object-oriented (oo) style approach/abstraction. If I look at my older tools, where I mostly do iterate directly over those array structures of the API, it seems to be obvious that I designed those not nicely. Having even a hard time to understand what’s going on in my own tool. This wouldn’t been the case for me, if I created an abstraction layer beforehand and named the functions, parameters, variables properly. I know that a lot of guys of you have no problem with that at all.

Also it makes a lot of sense that the api is representing the pattern data structure as it actually is. It’s a “near the metal” approach certainly. Still I was also “dreaming” of a tracker which is not bound to absolute numbers / indices, a bit like it started with Aodix, and then all only objects only, oo style structures, e.g. a note object containing all note related data automatically, e.g. per-note-automation, only normalized values, etc, already. I would imagine once you had this kind of song structure objects, how easy it would be to code a pattern view which would be zoomable for example. So everything then very elastic, flexible, and loosly bounded. But making a fast API for that certainly would be a difficult task then.

I am very thankful for what we have with Renoise. It’s the best tracker and one of the best daws existing. Simple as that.

1 Like

Don’t get me wrong, I know where you’re coming from. I often dream of a tracker that isn’t so beholden to tradition; indeed, of any DAW that isn’t hung up on one particular data representation. Aodix is a good shout—I never used it but it strikes me that it was exploring territories that remain largely unmapped to this day.

I am ambivalent about Renoise following these paths, as I think it gains much from its traditionalism, and the API concerns to me are irrelevant. Yes, if we want to do something un-Renoise-like, we have to work for it a bit.

But in general I resonate with your points about flexible structures, etc. These are things I think about a lot when pondering the DAWs of the future. :slight_smile:

2 Likes