Interactive Midi - Midi Parser & Player

Interactive Midi

InteractiveMidi is a module that takes the buffer of a midi file and allows you to play music with them. It comes with a Parser, Player, Visualizer, and Note Player.

Preview

Test Place

Methods:

Player

Read Only

Player.IsPlayer: boolean
Player.IsPlaying: boolean
Player.TimeLength: boolean

Editable Values

Player.Loop: boolean
Player.TimeMultiplier: boolean

Functions

Player:GetSongLength() -- To get the TimeLength you do Player.TimeLength after doing this
Player:Load(MidiClass)
Player:FireEvent(EventName, ...)
Player:SetTempo(tempo: number)
Player:HeartbeatStep(MidiClass, heartBeatDeltaTime: number)
Player:SetTimePosition(timePosition: number)
Player:Stop()
Player:Pause()
Player:Play()
Parser

Read Only

IsParser: boolean
ParseType: boolean

Editable Values
Parser.Events: {}

Functions

Parser:Load(Midi)
Parser:Parse(ParseType: string)
Midi

Read Only

IsMidi: boolean
Raw: {}
Header: {}

Editable Values
None

Functions

Midi.new(...)
Midi:Load(Buffer)
Binder

Read Only

IsBinder: boolean
BindingOrder: {}

Editable Values
None

Functions

Binder:Bind(...)
Binder:Load(Buffer)
Notator

Read Only
None

Editable Values

Instruments: {}
Percussion: {}

Functions

Notator:AddInstruments(instruments: { { SoundId: string, Offset: number } })
Notator:IsPercussion(noteNumber)
Notator:CreateSound(
	instrument: string,
	settings: {
		NoteNumber: number,
		Velocity: number,
		Offset: number, -- Optional
		CheckForPercussion: boolean
	}
)
Visualizer

Read Only
None

Editable Values
None

Functions

Visualizer:Basic(
	settings: {
		NoteNumber: number,
		Length: number,
		CenterCFrame: CFrame,
		
		Size: number,
		
		Z: number,
		
		Sharp: number,
		C: number
	}
) : BlockData

Editable Values (BlockData)

NoteLetter: number
Block: Instance

Functions (BlockData)

BlockData:Stop()
BlockData:SmoothHide(tweenTime)
BlockData:SmoothVisibility(tweenTime)
BlockData:StopGrow()
BlockData:MoveUp(height: number, tweenTime: number, grows: boolean)

How to use:

Parse Midi
local InteractiveMidi = require(script.InteractiveMidi) -- Require the InteractiveMidi Module

local MidiUrl = "https://github.com/Wyvern-Dev/Wyvern/raw/main/betterviva.mid" -- URL to Midi File, this is mine to Viva la Vida
local MidiBuffer = game:GetService("HttpService"):GetAsync(MidiUrl)

local Midi, Parser = InteractiveMidi.new("Midi", "Parser")

Midi:Load(MidiBuffer) -- Load The Buffer of The Midi File
Parser:Load(Midi) -- Load the Midi (getting it ready to parse)
Parser:Parse("Summation") --  Parses the Midi, there is only "Summation" currently, so you're kinda forced to put it.

print(Parser.Instructions) -- Print the Instructions

Shortened Version

local InteractiveMidi = require(script.InteractiveMidi) -- Require the InteractiveMidi Module

local MidiUrl = "https://github.com/Wyvern-Dev/Wyvern/raw/main/betterviva.mid" -- URL to Midi File, this is mine to Viva la Vida
local MidiBuffer = game:GetService("HttpService"):GetAsync(MidiUrl)

local Midi, Parser, Binder = InteractiveMidi.new("Midi", "Parser", "Binder")
Binder:Bind(Midi, { Parser, "Parse", "Summation" })
Binder:Load(MidiBuffer)

print(Parser.Instructions) -- Print the Instructions
Midi Player
local InteractiveMidi = require(script.InteractiveMidi) -- Require the InteractiveMidi Module

local MidiUrl = "https://github.com/Wyvern-Dev/Wyvern/raw/main/betterviva.mid" -- URL to Midi File, this is mine to Viva la Vida
local MidiBuffer = game:GetService("HttpService"):GetAsync(MidiUrl)

local Midi, Parser, Player, Notator = InteractiveMidi.new("Midi", "Parser", "Player", "Notator")

Midi:Load(MidiBuffer) -- Load The Buffer of The Midi File
Parser:Load(Midi) -- Load the Midi (getting it ready to parse)
Parser:Parse("Summation") --  Parses the Midi, there is only "Summation" currently, so you're kinda forced to put it.

Player:Load(Parser) -- Load The Parser into the Player

Player.Events.NoteOn = function(Instruction) -- Listen for When a note is played
	local IsPercussion = Instruction.Channel == 9 -- 9 is for Percussion notes
	
	local Sound: Sound = Notator:CreateSound("Piano", {
		NoteNumber = Instruction.NoteNumber,
		Velocity = Instruction.Velocity,
		CheckForPercussion = IsPercussion
	})
	
	Sound.Parent = workspace
	Sound:Play()
	
	game:GetService("Debris"):AddItem(Sound, 1)
end

Player:Play() -- Play the Midi

Shortened Version

local InteractiveMidi = require(script.InteractiveMidi) -- Require the InteractiveMidi Module

local MidiUrl = "https://github.com/Wyvern-Dev/Wyvern/raw/main/betterviva.mid" -- URL to Midi File, this is mine to Viva la Vida
local MidiBuffer = game:GetService("HttpService"):GetAsync(MidiUrl)

local Midi, Parser, Player, Notator, Binder = InteractiveMidi.new("Midi", "Parser", "Player", "Notator", "Binder")
Binder:Bind(Midi, { Parser, "Parse", "Summation" }, Player):Load(MidiBuffer)

Player.Events.NoteOn = function(Instruction) -- Listen for When a note is played
	local IsPercussion = Instruction.Channel == 9
	
	Instruction.CheckForPercussion = Instruction.Channel == 9
	local Sound: Sound = Notator:CreateSound("Piano", Instruction)
	Sound.Parent = workspace
	Sound:Play()
	
	game:GetService("Debris"):AddItem(Sound, 2)
end

Player:Play() -- Play the Midi
Visualizer

local InteractiveMidi = require(script.InteractiveMidi) -- Require the InteractiveMidi Module

local MidiUrl = "https://github.com/Wyvern-Dev/Wyvern/raw/main/betterviva.mid" -- URL to Midi File, this is mine to Viva la Vida
local MidiBuffer = game:GetService("HttpService"):GetAsync(MidiUrl)

local Midi, Parser, Player, Notator, Binder, Visualizer = InteractiveMidi.new("Midi", "Parser", "Player", "Notator", "Binder", "Visualizer")
Binder:Bind(Midi, { Parser, "Parse", "Summation" }, Player):Load(MidiBuffer)

Player.Events.NoteOn = function(Instruction) -- Listen for When a note is played
	local IsPercussion = Instruction.Channel == 9
	
	Instruction.CheckForPercussion = Instruction.Channel == 9
	local Sound: Sound = Notator:CreateSound("Piano", Instruction)
	Sound.Parent = workspace
	Sound:Play()
	
	game:GetService("Debris"):AddItem(Sound, 2)
	
	Instruction.Length = 1
	Instruction.CenterCFrame = CFrame.new(0, 10, 0)
	Instruction.Size = 1
	Instruction.Z = 0.1
	Instruction.Sharp = 0.5
	Instruction.C = 1
	
	local BlockData = Visualizer:Basic(Instruction):SmoothVisibility(0.1):MoveUp(100, 10, false)
	
	local Block = BlockData.Block
	Block.Material = Enum.Material.Neon
	Block.Color = Color3.fromRGB(255, 0, 0):Lerp(Color3.fromRGB(49, 193, 255), Instruction.NoteNumber/127)
	Block.Parent = workspace
	
	task.delay(9, BlockData.SmoothHide, BlockData, 0.5)
	game:GetService("Debris"):AddItem(Block, 10)
end

Player:Play() -- Play the Midi

Midi

All Events

InstructionEventData

InteractiveMidi.References.InstructionEventData = {
	SystemExclusive = { "Text" },
	TuneRequest = {},
	SongSelect = {},
	SongPosition = { "LSB", "MSB" },
	SequencerSpecific = { "Text" },
	
	KeySignature = { "Signature", "RelativeKey" },
	TimeSignature = { "Numerator", "Denominator", "ClockInMetronome", "Notated32ndNotesInQuarter" },
	
	SMPTEOffset = { "Hours", "Minutes", "Seconds", "Frames", "FractionalFrames" },
	SetTempo = { "Tempo" },
	
	SetSequenceNumber = { "SequenceNumber" },
	EndOfTrack = {},
	
	ChannelPrefix = { "Channel" },
	
	TextEvent = { "Text" },
	CopyrightText = { "Text" },
	TrackName = { "Text" },
	InstrumentName = { "Text" },
	Lyric = { "Text" },
	Marker = { "Text" },
	CuePoint = { "Text" },
	
	NoteOn = { "Channel", "NoteNumber", "Velocity" },
	NoteOff = { "Channel", "NoteNumber", "Velocity" },
	
	AfterTouchPoly = { "NoteNumber", "Touch" },
	ProgramChange = { "Channel", "PatchNumber" },
	ControlChange = { "ControllerNumber", "ControlChange" },
	AfterTouchChannel = { "Pressure" },
	PitchWheel = { "PitchBendLSB", "PitchBendMSB" },
}

InstructionEventBytes

InteractiveMidi.References.InstructionEventBytes = {
	-- Meta-Events --
	["F7"] = { "SystemExclusive", { "LENGTH" } },
	["F6"] = { "TuneRequest", {} },
	["F3"] = { "SongSelect", {} },
	["F2"] = { "SongPosition", { 2, 2 } },
	["F0"] = { "SystemExclusive", { "LENGTH" } },
	
	["7F"] = { "SequencerSpecific", { "LENGTH" } },
	
	["59"] = { "KeySignature", { 2, 2 } },
	["58"] = { "TimeSignature", { 2, 2, 2, 2 } },
	
	["54"] = { "SMPTEOffset", { 2, 2, 2, 2, 2 } },
	["51"] = { "SetTempo", { 6 } },
	
	["00"] = { "SetSequenceNumber", { 2 } },
	["2F"] = { "EndOfTrack", {} },
	
	["20"] = { "ChannelPrefix", { 2 } },
	["21"] = { "CopyrightText", { "LENGTH" } },
	
	-- Text Events --
	["01"] = { "TextEvent", { "LENGTH" } },
	["02"] = { "CopyrightText", { "LENGTH" } },
	["03"] = { "TrackName", { "LENGTH" } },
	["04"] = { "InstrumentName", { "LENGTH" } },
	["05"] = { "Lyric", { "LENGTH" } },
	["06"] = { "Marker", { "LENGTH" } },
	["07"] = { "CuePoint", { "LENGTH" } },
	
	-- Reserved/Unassigned Text Events --
	["08"] = { "TextEvent", { "LENGTH" } },
	["09"] = { "TextEvent", { "LENGTH" } },
	["0A"] = { "TextEvent", { "LENGTH" } },
	["0B"] = { "TextEvent", { "LENGTH" } },
	["0C"] = { "TextEvent", { "LENGTH" } },
	["0D"] = { "TextEvent", { "LENGTH" } },
	["0E"] = { "TextEvent", { "LENGTH" } },
	["0F"] = { "TextEvent", { "LENGTH" } },
	
	-- Midi-Events --
	["9"] = { "NoteOn", { 2, 2 } },
	["8"] = { "NoteOff", { 2, 2 } },
	
	["A"] = { "AfterTouchPoly", { 2, 2 } },
	["B"] = { "ControlChange", { 2, 2 } },
	["C"] = { "ProgramChange", { 2 } },
	["D"] = { "AfterTouchChannel", { 2 } },
	["E"] = { "PitchWheel", { 2, 2 } }
}

Get InteractiveMidi Here (Latest)
InteractiveMidi Model

Or

Version of this Post
InteractiveMidi.rbxm (11.3 KB)

Demo Place:
DemoPlace.rbxl (119.8 KB)

There are many things that I haven’t even shown you could do with it, but the Demo places shows some cool things you can do. I might still update the model, probably adding a syncing feature. So keep a look out for that.

This is my first module and, I created this module because I have no idea how other midi parsers/players work on Roblox and it was very hard to edit and read code. So I made my own.

I couldn’t find a tutorial on how to read the raw midi files and execute it into instructions. It was really hard and took me around a week just the finally get here. Feel free to read the code, the main stuff is going on in the Midi Parser.

Should I make it so you can create a midi file?

Similar to this

  • Yes
  • No

0 voters

52 Likes

Also in the demo place if you have match rotation for you selection when you drag. You can drag the white block around to change the time position. Enabling and Disabling Grow shows when the notes stop. Its wip.

11 Likes

This is very cool! Thanks for making this :smile:

I uh added Sync and Desync the the Midi Player
so
Player:Sync(Player2)
Player:Desync()

Player.SyncingThreshold: number – when the difference between the Player’s reach this number it resyncs back together. There shouldn’t be any Syncing Differences though.

Player.SyncingDifference: number – Tells you how far aparts you are from the other player. (Player2.TimePosition - Player1.TimePosition)

4 Likes

Nice, next try adding instruments in!


made instruments

5 Likes


Added a WaitEvery Feature so when parsing a big file you dont crash

Parser.WaitEvery: number – Wait every number of bytes
Parser.WaitTime: number – how long it waits in seconds

2 Likes

I made this nice thing using your module >:]

you can join the experience here if you hate the video quality!

7 Likes

absolutely hated playing, thank you!

3 Likes

anyone who wants to play the game with instruments, in the previews i was playing

1 Like

OH MY EARS I mean, it sounds great!

image

Nicee
btw i love “Viva La Vida”, and this made my day! :))

1 Like

Sorry, you should be able to now.

I made it copy unlocked, so you guys can now download the place

You can search up midi songs
but you can use these i have

there is this website you can use

to copy a midi url copy the link here



1 Like

you can alos move this bar (drag)

image

all values you see in there like 200k max buffer size you can change to like inf i just set it to 200k so i could test and also for NPS (notes per second)

It works. Great job.

In drums if you are synced with another player, it will play the midi version of the drum, but if you arent then it’ll just play the notes of the midi onto the drum which might sound weird

Is there a way to get the duration that each note should be sustained for?

Dependent on the actual midi file itself

You can create events for like NoteOn and NoteOff that’ll tell you when the note starts and ends. You can Play and stop the sound. Or if you want to just do sustain on Note then just listen for NoteOn and either get a long sound and play it or add reverb to the sound. You dont have to use the Notator it just helps with automatic sounds. but if you have sounds yourself you can customize it any way you want.

Interactive Midi should be customizable as much as possible